mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-09-10 22:41:05 +03:00
Compare commits
123 Commits
fix-textur
...
webrtc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7dc127c00f | ||
|
|
ad2efb8f2e | ||
|
|
7bab2c3297 | ||
|
|
dd5f4cd866 | ||
|
|
72fe4878d4 | ||
|
|
f94c861182 | ||
|
|
fe2946617e | ||
|
|
aef7d9758b | ||
|
|
231ccae10d | ||
|
|
62b2b67bfd | ||
|
|
ab89f2338c | ||
|
|
810b7aef76 | ||
|
|
88d5172611 | ||
|
|
3a62d81573 | ||
|
|
b6b4d4f036 | ||
|
|
5f3a046188 | ||
|
|
188b02ed2f | ||
|
|
90833ec315 | ||
|
|
a4f48e3631 | ||
|
|
18a8f9ac85 | ||
|
|
5ff93aafb4 | ||
|
|
7712e66540 | ||
|
|
44d4fb59b2 | ||
|
|
14dc121d23 | ||
|
|
8148795f19 | ||
|
|
a35630b499 | ||
|
|
cbec70cd6a | ||
|
|
ccf9afd069 | ||
|
|
ea7407b73b | ||
|
|
7fe4d186e5 | ||
|
|
d60577e80d | ||
|
|
704f7495b9 | ||
|
|
3f5ce9acae | ||
|
|
b962063f03 | ||
|
|
06a7cc9239 | ||
|
|
5618e984b6 | ||
|
|
578a95289f | ||
|
|
22c4e080bb | ||
|
|
585d1fb3ca | ||
|
|
c313d6dc1c | ||
|
|
fa399e01ad | ||
|
|
324b58e04e | ||
|
|
8ad7c257cf | ||
|
|
d5cf646db3 | ||
|
|
3ca5465689 | ||
|
|
872ec56602 | ||
|
|
9b986be5a0 | ||
|
|
4dfef0e632 | ||
|
|
20d5fd8c58 | ||
|
|
f9ecc48b2e | ||
|
|
b7f6789a8c | ||
|
|
64c2ad4d5a | ||
|
|
a4491c7ad1 | ||
|
|
3fc11c0f81 | ||
|
|
618bf37deb | ||
|
|
c1a587cfa4 | ||
|
|
9a1c8da143 | ||
|
|
978c901f49 | ||
|
|
d453a19601 | ||
|
|
50c4e435de | ||
|
|
d5c6d0f6b7 | ||
|
|
b6ff62c74b | ||
|
|
ba6de7990f | ||
|
|
a59ad333fc | ||
|
|
82aa28f129 | ||
|
|
3f93005be2 | ||
|
|
23a147b0dc | ||
|
|
e4539fc304 | ||
|
|
6dbd810454 | ||
|
|
0fd1a0eecb | ||
|
|
dfb5804dd0 | ||
|
|
957dfe8c96 | ||
|
|
c312385ffd | ||
|
|
f28ac38ccf | ||
|
|
28cf1836e6 | ||
|
|
1ec1b9e7e3 | ||
|
|
2c84c8fb13 | ||
|
|
66ab0b87f6 | ||
|
|
169f74f8d9 | ||
|
|
d4b06a6c5c | ||
|
|
03a7fc5992 | ||
|
|
1fe451c2e8 | ||
|
|
0b08a83d4b | ||
|
|
e9b81e3475 | ||
|
|
7220f00410 | ||
|
|
fd471fcf02 | ||
|
|
7c6e661fcc | ||
|
|
3f207e91f6 | ||
|
|
cec4085238 | ||
|
|
0d917c6fa1 | ||
|
|
893dc27798 | ||
|
|
7cc82c1575 | ||
|
|
f07b6e2338 | ||
|
|
a3bab27a2a | ||
|
|
7423dced37 | ||
|
|
a7deef02a2 | ||
|
|
d5a7f67999 | ||
|
|
e266380ee9 | ||
|
|
cbf9440281 | ||
|
|
6eaac17ac5 | ||
|
|
92eb137178 | ||
|
|
61ddade049 | ||
|
|
c78bdefc44 | ||
|
|
c45f7d2dd2 | ||
|
|
798b73beb1 | ||
|
|
f1a06f6765 | ||
|
|
0a4b431ea2 | ||
|
|
5679670506 | ||
|
|
630b531108 | ||
|
|
1984678785 | ||
|
|
b0008edcb5 | ||
|
|
0c00d576dd | ||
|
|
9b1b810d3a | ||
|
|
6a27910f34 | ||
|
|
14a4b197ad | ||
|
|
8ffe3117a5 | ||
|
|
5a78be03e3 | ||
|
|
edd0e5fbd4 | ||
|
|
3871c47855 | ||
|
|
8d52d48b24 | ||
|
|
7aa98d43cf | ||
|
|
d1da05c4db | ||
|
|
d829d1410a |
11
.github/dependabot.yml
vendored
11
.github/dependabot.yml
vendored
@@ -1,11 +0,0 @@
|
|||||||
version: 2
|
|
||||||
updates:
|
|
||||||
- package-ecosystem: "gitsubmodule"
|
|
||||||
directory: "/"
|
|
||||||
target-branch: "master"
|
|
||||||
schedule:
|
|
||||||
interval: "daily"
|
|
||||||
commit-message:
|
|
||||||
prefix: "Git submodule"
|
|
||||||
labels:
|
|
||||||
- "dependencies"
|
|
||||||
3
.github/workflows/ci.yml
vendored
3
.github/workflows/ci.yml
vendored
@@ -5,7 +5,7 @@ env:
|
|||||||
# CICD_INTERMEDIATES_DIR: "_cicd-intermediates"
|
# CICD_INTERMEDIATES_DIR: "_cicd-intermediates"
|
||||||
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
|
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
|
||||||
# for multiarch gcc compatibility
|
# for multiarch gcc compatibility
|
||||||
VCPKG_COMMIT_ID: "120deac3062162151622ca4860575a33844ba10b"
|
VCPKG_COMMIT_ID: "9e593bb18ea69cc5095e012465dcd675a822ed0d"
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
@@ -124,7 +124,6 @@ jobs:
|
|||||||
gcc \
|
gcc \
|
||||||
git \
|
git \
|
||||||
g++ \
|
g++ \
|
||||||
libpam0g-dev \
|
|
||||||
libasound2-dev \
|
libasound2-dev \
|
||||||
libunwind-dev \
|
libunwind-dev \
|
||||||
libgstreamer1.0-dev \
|
libgstreamer1.0-dev \
|
||||||
|
|||||||
100
.github/workflows/flutter-build.yml
vendored
100
.github/workflows/flutter-build.yml
vendored
@@ -31,20 +31,21 @@ env:
|
|||||||
# engine is 3.44. Every other platform stays on FLUTTER_VERSION (3.24.5) until Windows 7
|
# engine is 3.44. Every other platform stays on FLUTTER_VERSION (3.24.5) until Windows 7
|
||||||
# support is restored after the upstream-wide Flutter bump. The arm64 job patches the few
|
# support is restored after the upstream-wide Flutter bump. The arm64 job patches the few
|
||||||
# 3.44-only source/pubspec changes on the fly (see "Patch RustDesk sources for Flutter 3.44").
|
# 3.44-only source/pubspec changes on the fly (see "Patch RustDesk sources for Flutter 3.44").
|
||||||
FLUTTER_WINDOWS_ARM_VERSION: "3.44.8"
|
FLUTTER_WINDOWS_ARM_VERSION: "3.44.9"
|
||||||
# for arm64 linux because official Dart SDK does not work
|
# for arm64 linux because official Dart SDK does not work
|
||||||
FLUTTER_ELINUX_VERSION: "3.16.9"
|
FLUTTER_ELINUX_VERSION: "3.16.9"
|
||||||
TAG_NAME: "${{ inputs.upload-tag }}"
|
TAG_NAME: "${{ inputs.upload-tag }}"
|
||||||
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
|
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
|
||||||
# vcpkg version: 2025.08.27
|
# vcpkg version: 2026.07.29
|
||||||
# If we change the `VCPKG COMMIT_ID`, please remember:
|
# If we change the `VCPKG COMMIT_ID`, please remember:
|
||||||
# 1. Call `$VCPKG_ROOT/vcpkg x-update-baseline` to update the baseline in `vcpkg.json`.
|
# 1. Call `$VCPKG_ROOT/vcpkg x-update-baseline` to update the baseline in `vcpkg.json`.
|
||||||
# Or we may face build issue like
|
# Or we may face build issue like
|
||||||
# https://github.com/rustdesk/rustdesk/actions/runs/14414119794/job/40427970174
|
# https://github.com/rustdesk/rustdesk/actions/runs/14414119794/job/40427970174
|
||||||
# 2. Update the `VCPKG_COMMIT_ID` in `ci.yml` and `playground.yml`.
|
# 2. Update the `VCPKG_COMMIT_ID` in `ci.yml` and `playground.yml`.
|
||||||
VCPKG_COMMIT_ID: "120deac3062162151622ca4860575a33844ba10b"
|
VCPKG_COMMIT_ID: "9e593bb18ea69cc5095e012465dcd675a822ed0d"
|
||||||
|
VCPKG_CMAKE_VERSION: "4.3.0"
|
||||||
ARMV7_VCPKG_COMMIT_ID: "6f29f12e82a8293156836ad81cc9bf5af41fe836" # 2025.01.13, got "/opt/artifacts/vcpkg/vcpkg: No such file or directory" with latest version
|
ARMV7_VCPKG_COMMIT_ID: "6f29f12e82a8293156836ad81cc9bf5af41fe836" # 2025.01.13, got "/opt/artifacts/vcpkg/vcpkg: No such file or directory" with latest version
|
||||||
VERSION: "1.4.9"
|
VERSION: "1.5.0"
|
||||||
NDK_VERSION: "r28c"
|
NDK_VERSION: "r28c"
|
||||||
#signing keys env variable checks
|
#signing keys env variable checks
|
||||||
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
|
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
|
||||||
@@ -389,6 +390,54 @@ jobs:
|
|||||||
mv $msi.FullName ../../SignOutput/rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.msi
|
mv $msi.FullName ../../SignOutput/rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.msi
|
||||||
sha256sum ../../SignOutput/rustdesk-*.msi
|
sha256sum ../../SignOutput/rustdesk-*.msi
|
||||||
|
|
||||||
|
- name: Build pre-built MSI template
|
||||||
|
# Two things this works around: preprocess.py rewrites res/msi in place, so the
|
||||||
|
# tree is reset around this second variant; and it locates the app as
|
||||||
|
# <app-name>.exe inside the dist, so the dist copy is renamed to match.
|
||||||
|
#
|
||||||
|
# The placeholder is chosen to keep this template as close to the shipped msi as
|
||||||
|
# possible: eight characters like "RustDesk", and a valid 8.3 name, so WiX
|
||||||
|
# derives no short name for it. A longer placeholder would get one, and a patch
|
||||||
|
# cannot rewrite a truncated placeholder, leaving short names pointing at it.
|
||||||
|
#
|
||||||
|
# It still has to be unique, which is why "RustDesk" itself cannot be used:
|
||||||
|
# it also names payload that must never be renamed, such as librustdesk.dll
|
||||||
|
# and drivers\RustDeskPrinterDriver.
|
||||||
|
#
|
||||||
|
#
|
||||||
|
# Building the arm64 template on the native arm64 runner makes the ARM
|
||||||
|
# package available: the build agents are x64 and cannot run
|
||||||
|
# preprocess.py against an ARM exe.
|
||||||
|
if: env.UPLOAD_ARTIFACT == 'true'
|
||||||
|
run: |
|
||||||
|
git checkout -- res/msi
|
||||||
|
cp -r ./rustdesk ./rustdesk-msi-template
|
||||||
|
mv ./rustdesk-msi-template/rustdesk.exe ./rustdesk-msi-template/RDAPPNAM.exe
|
||||||
|
Set-Content -Path ./rustdesk-msi-template/custom.txt -Value 'placeholder' -NoNewline
|
||||||
|
$assets = './rustdesk-msi-template/data/flutter_assets/assets'
|
||||||
|
New-Item -ItemType Directory -Force -Path $assets | Out-Null
|
||||||
|
foreach ($a in 'icon.ico','icon.png','logo.png','logo_light.png','logo_dark.png') {
|
||||||
|
Set-Content -Path "$assets/$a" -Value 'placeholder' -NoNewline
|
||||||
|
}
|
||||||
|
pushd ./res/msi
|
||||||
|
python preprocess.py --arp --template --revision-version 0 -d ../../rustdesk-msi-template --app-name RDAPPNAM
|
||||||
|
$msiPlatform = if ('${{ matrix.job.arch }}' -eq 'aarch64') { 'ARM64' } else { 'x64' }
|
||||||
|
msbuild msi.sln -t:clean -p:Configuration=Release -p:Platform=$msiPlatform
|
||||||
|
msbuild msi.sln -p:Configuration=Release -p:Platform=$msiPlatform /p:TargetVersion=Windows10
|
||||||
|
$msi = Get-ChildItem ./Package/bin/*/Release/en-us/Package.msi | Select-Object -First 1
|
||||||
|
popd
|
||||||
|
mkdir ./msi-template
|
||||||
|
mv $msi.FullName ./msi-template/rustdesk-template-${{ matrix.job.arch }}.msi
|
||||||
|
git checkout -- res/msi
|
||||||
|
rm -r -fo ./rustdesk-msi-template
|
||||||
|
|
||||||
|
- name: Upload unsigned msi template
|
||||||
|
if: env.UPLOAD_ARTIFACT == 'true'
|
||||||
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
|
with:
|
||||||
|
name: rustdesk-unsigned-msi-template-${{ matrix.job.arch }}
|
||||||
|
path: ./msi-template
|
||||||
|
|
||||||
- name: Sign rustdesk self-extracted file
|
- name: Sign rustdesk self-extracted file
|
||||||
if: env.UPLOAD_ARTIFACT == 'true' && env.SIGN_BASE_URL != '-2'
|
if: env.UPLOAD_ARTIFACT == 'true' && env.SIGN_BASE_URL != '-2'
|
||||||
shell: bash
|
shell: bash
|
||||||
@@ -925,15 +974,33 @@ jobs:
|
|||||||
name: rustdesk-unsigned-windows-x86_64
|
name: rustdesk-unsigned-windows-x86_64
|
||||||
path: ./windows-x86_64/
|
path: ./windows-x86_64/
|
||||||
|
|
||||||
|
- name: Download Artifacts
|
||||||
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||||
|
with:
|
||||||
|
name: rustdesk-unsigned-windows-aarch64
|
||||||
|
path: ./windows-aarch64/
|
||||||
|
|
||||||
- name: Download Artifacts
|
- name: Download Artifacts
|
||||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||||
with:
|
with:
|
||||||
name: rustdesk-unsigned-windows-x86
|
name: rustdesk-unsigned-windows-x86
|
||||||
path: ./windows-x86/
|
path: ./windows-x86/
|
||||||
|
|
||||||
|
- name: Download Artifacts
|
||||||
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||||
|
with:
|
||||||
|
name: rustdesk-unsigned-msi-template-x86_64
|
||||||
|
path: ./msi-template/
|
||||||
|
|
||||||
|
- name: Download Artifacts
|
||||||
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||||
|
with:
|
||||||
|
name: rustdesk-unsigned-msi-template-aarch64
|
||||||
|
path: ./msi-template/
|
||||||
|
|
||||||
- name: Combine unsigned app
|
- name: Combine unsigned app
|
||||||
run: |
|
run: |
|
||||||
tar czf rustdesk-${{ env.VERSION }}-unsigned.tar.gz *.dmg windows-x86_64 windows-x86
|
tar czf rustdesk-${{ env.VERSION }}-unsigned.tar.gz *.dmg windows-x86_64 windows-aarch64 windows-x86 msi-template
|
||||||
|
|
||||||
- name: Publish unsigned app
|
- name: Publish unsigned app
|
||||||
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
|
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
|
||||||
@@ -1009,7 +1076,6 @@ jobs:
|
|||||||
libgstreamer1.0-dev \
|
libgstreamer1.0-dev \
|
||||||
libgstreamer-plugins-base1.0-dev \
|
libgstreamer-plugins-base1.0-dev \
|
||||||
libgtk-3-dev \
|
libgtk-3-dev \
|
||||||
libpam0g-dev \
|
|
||||||
libpulse-dev \
|
libpulse-dev \
|
||||||
libva-dev \
|
libva-dev \
|
||||||
libxcb-randr0-dev \
|
libxcb-randr0-dev \
|
||||||
@@ -1283,7 +1349,6 @@ jobs:
|
|||||||
libgstreamer1.0-dev \
|
libgstreamer1.0-dev \
|
||||||
libgstreamer-plugins-base1.0-dev \
|
libgstreamer-plugins-base1.0-dev \
|
||||||
libgtk-3-dev \
|
libgtk-3-dev \
|
||||||
libpam0g-dev \
|
|
||||||
libpulse-dev \
|
libpulse-dev \
|
||||||
libva-dev \
|
libva-dev \
|
||||||
libxcb-randr0-dev \
|
libxcb-randr0-dev \
|
||||||
@@ -1472,7 +1537,6 @@ jobs:
|
|||||||
submodules: recursive
|
submodules: recursive
|
||||||
|
|
||||||
- name: Set Swap Space
|
- name: Set Swap Space
|
||||||
if: ${{ matrix.job.arch == 'x86_64' }}
|
|
||||||
uses: pierotofy/set-swap-space@49819abfb41bd9b44fb781159c033dba90353a7c # v1.0
|
uses: pierotofy/set-swap-space@49819abfb41bd9b44fb781159c033dba90353a7c # v1.0
|
||||||
with:
|
with:
|
||||||
swap-size-gb: 12
|
swap-size-gb: 12
|
||||||
@@ -1507,6 +1571,15 @@ jobs:
|
|||||||
name: bridge-artifact
|
name: bridge-artifact
|
||||||
path: ./
|
path: ./
|
||||||
|
|
||||||
|
# vcpkg 2026.07.29's SPDX scripts require CMake 4.3+, but this ARM64 runner selects CMake 3.31.
|
||||||
|
- name: Install CMake for vcpkg on Linux ARM64
|
||||||
|
if: matrix.job.arch == 'aarch64' && env.UPLOAD_ARTIFACT == 'true'
|
||||||
|
run: |
|
||||||
|
python3 -m pip install --user "cmake==${VCPKG_CMAKE_VERSION}"
|
||||||
|
user_base="$(python3 -m site --user-base)"
|
||||||
|
"${user_base}/bin/cmake" --version
|
||||||
|
echo "${user_base}/bin" >> "${GITHUB_PATH}"
|
||||||
|
|
||||||
- name: Setup vcpkg with Github Actions binary cache
|
- name: Setup vcpkg with Github Actions binary cache
|
||||||
if: matrix.job.arch == 'x86_64' || env.UPLOAD_ARTIFACT == 'true'
|
if: matrix.job.arch == 'x86_64' || env.UPLOAD_ARTIFACT == 'true'
|
||||||
uses: lukka/run-vcpkg@b1a0dd252f06b9e25b3c022a9a03bd7a427fb6a2 # v11
|
uses: lukka/run-vcpkg@b1a0dd252f06b9e25b3c022a9a03bd7a427fb6a2 # v11
|
||||||
@@ -1574,7 +1647,6 @@ jobs:
|
|||||||
libgstreamer1.0-dev \
|
libgstreamer1.0-dev \
|
||||||
libgstreamer-plugins-base1.0-dev \
|
libgstreamer-plugins-base1.0-dev \
|
||||||
libgtk-3-dev \
|
libgtk-3-dev \
|
||||||
libpam0g-dev \
|
|
||||||
libpulse-dev \
|
libpulse-dev \
|
||||||
libva-dev \
|
libva-dev \
|
||||||
libxcb-randr0-dev \
|
libxcb-randr0-dev \
|
||||||
@@ -1896,7 +1968,6 @@ jobs:
|
|||||||
libgstreamer1.0-dev \
|
libgstreamer1.0-dev \
|
||||||
libgstreamer-plugins-base1.0-dev \
|
libgstreamer-plugins-base1.0-dev \
|
||||||
libgtk-3-dev \
|
libgtk-3-dev \
|
||||||
libpam0g-dev \
|
|
||||||
libpulse-dev \
|
libpulse-dev \
|
||||||
libva-dev \
|
libva-dev \
|
||||||
libxcb-randr0-dev \
|
libxcb-randr0-dev \
|
||||||
@@ -2079,6 +2150,12 @@ jobs:
|
|||||||
echo "Modified vcpkg.json for armv7 build:"
|
echo "Modified vcpkg.json for armv7 build:"
|
||||||
grep -A 2 -B 2 '"baseline"' vcpkg.json
|
grep -A 2 -B 2 '"baseline"' vcpkg.json
|
||||||
|
|
||||||
|
- name: Set Swap Space
|
||||||
|
if: matrix.job.arch == 'armv7'
|
||||||
|
uses: pierotofy/set-swap-space@49819abfb41bd9b44fb781159c033dba90353a7c # v1.0
|
||||||
|
with:
|
||||||
|
swap-size-gb: 12
|
||||||
|
|
||||||
- name: Free Space
|
- name: Free Space
|
||||||
run: |
|
run: |
|
||||||
df -h
|
df -h
|
||||||
@@ -2126,7 +2203,6 @@ jobs:
|
|||||||
libgstreamer-plugins-base1.0-dev \
|
libgstreamer-plugins-base1.0-dev \
|
||||||
libgtk-3-dev \
|
libgtk-3-dev \
|
||||||
liblzma-dev \
|
liblzma-dev \
|
||||||
libpam0g-dev \
|
|
||||||
libpulse-dev \
|
libpulse-dev \
|
||||||
libva-dev \
|
libva-dev \
|
||||||
libxcb-randr0-dev \
|
libxcb-randr0-dev \
|
||||||
@@ -2203,7 +2279,7 @@ jobs:
|
|||||||
mkdir -p ~/.cargo/
|
mkdir -p ~/.cargo/
|
||||||
echo """
|
echo """
|
||||||
[source.crates-io]
|
[source.crates-io]
|
||||||
registry = 'https://github.com/rust-lang/crates.io-index'
|
registry = 'sparse+https://index.crates.io/'
|
||||||
""" > ~/.cargo/config
|
""" > ~/.cargo/config
|
||||||
cat ~/.cargo/config
|
cat ~/.cargo/config
|
||||||
# install dependencies from vcpkg
|
# install dependencies from vcpkg
|
||||||
|
|||||||
11
.github/workflows/playground.yml
vendored
11
.github/workflows/playground.yml
vendored
@@ -16,8 +16,8 @@ env:
|
|||||||
FLUTTER_ELINUX_VERSION: "3.16.9"
|
FLUTTER_ELINUX_VERSION: "3.16.9"
|
||||||
TAG_NAME: "nightly"
|
TAG_NAME: "nightly"
|
||||||
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
|
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
|
||||||
VCPKG_COMMIT_ID: "120deac3062162151622ca4860575a33844ba10b"
|
VCPKG_COMMIT_ID: "9e593bb18ea69cc5095e012465dcd675a822ed0d"
|
||||||
VERSION: "1.4.9"
|
VERSION: "1.5.0"
|
||||||
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 }}"
|
||||||
@@ -271,7 +271,6 @@ jobs:
|
|||||||
libgstreamer1.0-dev \
|
libgstreamer1.0-dev \
|
||||||
libgstreamer-plugins-base1.0-dev \
|
libgstreamer-plugins-base1.0-dev \
|
||||||
libgtk-3-dev \
|
libgtk-3-dev \
|
||||||
libpam0g-dev \
|
|
||||||
libpulse-dev \
|
libpulse-dev \
|
||||||
libva-dev \
|
libva-dev \
|
||||||
libvdpau-dev \
|
libvdpau-dev \
|
||||||
@@ -284,7 +283,7 @@ jobs:
|
|||||||
nasm \
|
nasm \
|
||||||
yasm \
|
yasm \
|
||||||
ninja-build \
|
ninja-build \
|
||||||
openjdk-11-jdk-headless \
|
openjdk-17-jdk-headless \
|
||||||
pkg-config \
|
pkg-config \
|
||||||
tree \
|
tree \
|
||||||
wget
|
wget
|
||||||
@@ -366,9 +365,9 @@ jobs:
|
|||||||
- name: Build rustdesk
|
- name: Build rustdesk
|
||||||
shell: bash
|
shell: bash
|
||||||
env:
|
env:
|
||||||
JAVA_HOME: /usr/lib/jvm/java-11-openjdk-amd64
|
JAVA_HOME: /usr/lib/jvm/java-17-openjdk-amd64
|
||||||
run: |
|
run: |
|
||||||
export PATH=/usr/lib/jvm/java-11-openjdk-amd64/bin:$PATH
|
export PATH=/usr/lib/jvm/java-17-openjdk-amd64/bin:$PATH
|
||||||
# temporary use debug sign config
|
# temporary use debug sign config
|
||||||
sed -i "s/signingConfigs.release/signingConfigs.debug/g" ./flutter/android/app/build.gradle
|
sed -i "s/signingConfigs.release/signingConfigs.debug/g" ./flutter/android/app/build.gradle
|
||||||
case ${{ matrix.job.target }} in
|
case ${{ matrix.job.target }} in
|
||||||
|
|||||||
4
.github/workflows/update-webpki-roots.yml
vendored
4
.github/workflows/update-webpki-roots.yml
vendored
@@ -33,6 +33,10 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout source code
|
- name: Checkout source code
|
||||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||||
|
with:
|
||||||
|
# The root workspace lists libs/hbb_common as a member; without the
|
||||||
|
# submodule its manifest is missing and cargo cannot load the workspace.
|
||||||
|
submodules: recursive
|
||||||
|
|
||||||
- name: Update webpki-roots in all lockfiles
|
- name: Update webpki-roots in all lockfiles
|
||||||
id: update
|
id: update
|
||||||
|
|||||||
34
AGENTS.md
34
AGENTS.md
@@ -63,17 +63,43 @@
|
|||||||
|
|
||||||
### Comments
|
### Comments
|
||||||
|
|
||||||
* Keep them short: one line by default, three at most.
|
* Avoid comments unless they explain a non-obvious reason, constraint, or workaround.
|
||||||
* Say **why**, never what. If the code already says it, delete the comment.
|
* Never restate what the code does; prefer clearer code instead.
|
||||||
* A comment must never be longer than the code it describes.
|
* If the code is self-explanatory, add no comment.
|
||||||
* Applies to YAML, shell and Python too, not just Rust.
|
|
||||||
|
|
||||||
### Be minimally invasive
|
### Be minimally invasive
|
||||||
|
|
||||||
* Prefer purely additive changes: layer new (`#[cfg]`-gated) blocks or new functions around existing code instead of restructuring it. The ideal diff for a fix adds lines and modifies/deletes none.
|
* Prefer purely additive changes: layer new (`#[cfg]`-gated) blocks or new functions around existing code instead of restructuring it. The ideal diff for a fix adds lines and modifies/deletes none.
|
||||||
* Do not extract or reshape existing code just to enable your new code; look for a mechanism that leaves existing lines untouched (e.g. hide/show an existing object instead of refactoring its construction into a helper for rebuilding).
|
* Do not extract or reshape existing code just to enable your new code; look for a mechanism that leaves existing lines untouched (e.g. hide/show an existing object instead of refactoring its construction into a helper for rebuilding).
|
||||||
|
* Accept a little duplication over a restructure. A new function that repeats a few lines of an existing one is a better diff than reshaping the original so both can share it.
|
||||||
* Put new logic in self-contained functions in the module it belongs to (platform-specific logic in `src/platform/`, with `use` inside the function body to avoid churning shared import blocks). Call sites in shared files (`src/tray.rs`, `src/core_main.rs`, `src/server/connection.rs`, …) should be thin one-line hooks.
|
* Put new logic in self-contained functions in the module it belongs to (platform-specific logic in `src/platform/`, with `use` inside the function body to avoid churning shared import blocks). Call sites in shared files (`src/tray.rs`, `src/core_main.rs`, `src/server/connection.rs`, …) should be thin one-line hooks.
|
||||||
|
|
||||||
|
### Scope check before touching shared code
|
||||||
|
|
||||||
|
* Before changing a shared trait, a shared struct, or the signature of a widely used function, check whether the bug or feature is specific to one path. If it is, keep the change inside that path unless that is impossible, and say in the PR why it was.
|
||||||
|
* If an unrelated caller needs `Default::default()`, `None`, or another placeholder solely to satisfy a signature you changed, the diff is too broad: stop and redesign.
|
||||||
|
* The expected shape of a fix is a new function in the feature's own module, plus at most a new field or a thin hook in the shared code it needs. Feature-specific state belongs beside the feature's existing state, not in a new abstraction every caller has to learn.
|
||||||
|
|
||||||
|
### Mandatory regression-surface check
|
||||||
|
|
||||||
|
Before considering any implementation complete, perform a minimization pass over the final diff.
|
||||||
|
|
||||||
|
* Inspect every modified existing file and every modified existing code path. Each must be strictly necessary for the requested change. Revert changes that are merely cleanup, refactoring, consistency improvements, or fixes for pre-existing issues.
|
||||||
|
* For new features, preserve the existing implementation path when the feature is disabled or unsupported whenever practical. `feature off` should run the old code, not a rewritten equivalent.
|
||||||
|
* Do not route existing behavior through a new abstraction merely to share code with the new feature. Prefer a parallel new function or a small amount of duplication over changing a proven existing path.
|
||||||
|
* Keep new implementation logic in new or feature-specific modules. Changes to shared/core files should normally be thin hooks, capability checks, or protocol plumbing.
|
||||||
|
* Do not fix unrelated pre-existing bugs in the same PR. Put them in a separate change unless they directly block correctness or security of the requested work.
|
||||||
|
* For submodule bumps, inspect the exact commit range and ensure unrelated changes are not being pulled into the parent PR.
|
||||||
|
* Before finalizing, explicitly report the regression surface: list the existing files and existing runtime paths whose behavior changed, and explain why each change is unavoidable.
|
||||||
|
* During review, treat an unnecessarily modified legacy path as a review finding even if tests pass and the rewritten behavior appears equivalent.
|
||||||
|
|
||||||
|
## Reviewing a PR
|
||||||
|
|
||||||
|
* Review only what the diff introduces. Verify ownership with `gh pr diff` before reporting a finding — if the offending lines are untouched context, it is a pre-existing problem, not this PR's.
|
||||||
|
* List pre-existing problems in a separate section at the end, or leave out the ones that are not fatal. Never mix them into the findings the author has to fix.
|
||||||
|
* Before re-reviewing, read the author's reply comments. Do not re-raise items they declined on scope grounds.
|
||||||
|
* State a finding's consequence exactly: distinguish "the value is lost" from "the shortcut is inert but the value still saves".
|
||||||
|
|
||||||
## Localization (`src/lang/*.rs`)
|
## Localization (`src/lang/*.rs`)
|
||||||
|
|
||||||
Each file is a `HashMap<key, translation>`. Layout:
|
Each file is a `HashMap<key, translation>`. Layout:
|
||||||
|
|||||||
450
Cargo.lock
generated
450
Cargo.lock
generated
@@ -753,24 +753,6 @@ dependencies = [
|
|||||||
"syn 2.0.98",
|
"syn 2.0.98",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "bindgen"
|
|
||||||
version = "0.71.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "5f58bf3d7db68cfbac37cfc485a8d711e87e064c3d0fe0435b92f7a407f9d6b3"
|
|
||||||
dependencies = [
|
|
||||||
"bitflags 2.9.1",
|
|
||||||
"cexpr",
|
|
||||||
"clang-sys",
|
|
||||||
"itertools 0.12.1",
|
|
||||||
"proc-macro2 1.0.93",
|
|
||||||
"quote 1.0.36",
|
|
||||||
"regex",
|
|
||||||
"rustc-hash 2.1.1",
|
|
||||||
"shlex",
|
|
||||||
"syn 2.0.98",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bindgen"
|
name = "bindgen"
|
||||||
version = "0.72.1"
|
version = "0.72.1"
|
||||||
@@ -986,27 +968,6 @@ dependencies = [
|
|||||||
"serde 1.0.228",
|
"serde 1.0.228",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "bzip2"
|
|
||||||
version = "0.4.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "bdb116a6ef3f6c3698828873ad02c3014b3c85cadb88496095628e3ef1e347f8"
|
|
||||||
dependencies = [
|
|
||||||
"bzip2-sys",
|
|
||||||
"libc",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "bzip2-sys"
|
|
||||||
version = "0.1.11+1.0.8"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "736a955f3fa7875102d57c82b8cac37ec45224a07fd32d58f9f7a186b6cd4cdc"
|
|
||||||
dependencies = [
|
|
||||||
"cc",
|
|
||||||
"libc",
|
|
||||||
"pkg-config",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cacao"
|
name = "cacao"
|
||||||
version = "0.4.0-beta2"
|
version = "0.4.0-beta2"
|
||||||
@@ -1182,30 +1143,6 @@ version = "0.2.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "chacha20"
|
|
||||||
version = "0.9.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818"
|
|
||||||
dependencies = [
|
|
||||||
"cfg-if 1.0.0",
|
|
||||||
"cipher",
|
|
||||||
"cpufeatures",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "chacha20poly1305"
|
|
||||||
version = "0.10.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35"
|
|
||||||
dependencies = [
|
|
||||||
"aead",
|
|
||||||
"chacha20",
|
|
||||||
"cipher",
|
|
||||||
"poly1305",
|
|
||||||
"zeroize",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "chrono"
|
name = "chrono"
|
||||||
version = "0.4.41"
|
version = "0.4.41"
|
||||||
@@ -1255,7 +1192,6 @@ checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"crypto-common",
|
"crypto-common",
|
||||||
"inout",
|
"inout",
|
||||||
"zeroize",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1477,8 +1413,8 @@ dependencies = [
|
|||||||
"compression-core",
|
"compression-core",
|
||||||
"flate2",
|
"flate2",
|
||||||
"memchr",
|
"memchr",
|
||||||
"zstd 0.13.1",
|
"zstd",
|
||||||
"zstd-safe 7.1.0",
|
"zstd-safe",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1549,12 +1485,6 @@ dependencies = [
|
|||||||
"unicode-xid 0.2.4",
|
"unicode-xid 0.2.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "constant_time_eq"
|
|
||||||
version = "0.1.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "constant_time_eq"
|
name = "constant_time_eq"
|
||||||
version = "0.2.6"
|
version = "0.2.6"
|
||||||
@@ -2351,7 +2281,7 @@ version = "0.5.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412"
|
checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libloading 0.8.4",
|
"libloading 0.7.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2469,42 +2399,6 @@ dependencies = [
|
|||||||
"linux-raw-sys 0.6.5",
|
"linux-raw-sys 0.6.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "dtls"
|
|
||||||
version = "0.13.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "f531dd7c181beaf3cebab3716afa4d0d41ab888be85232583f56bbaf07ca208a"
|
|
||||||
dependencies = [
|
|
||||||
"aes",
|
|
||||||
"aes-gcm",
|
|
||||||
"async-trait",
|
|
||||||
"bincode",
|
|
||||||
"byteorder",
|
|
||||||
"cbc",
|
|
||||||
"ccm",
|
|
||||||
"chacha20poly1305",
|
|
||||||
"der-parser",
|
|
||||||
"hmac",
|
|
||||||
"log",
|
|
||||||
"p256",
|
|
||||||
"p384",
|
|
||||||
"portable-atomic",
|
|
||||||
"rand 0.9.2",
|
|
||||||
"rand_core 0.6.4",
|
|
||||||
"rcgen",
|
|
||||||
"ring",
|
|
||||||
"rustls",
|
|
||||||
"sec1",
|
|
||||||
"serde 1.0.228",
|
|
||||||
"sha1",
|
|
||||||
"sha2",
|
|
||||||
"thiserror 1.0.61",
|
|
||||||
"tokio",
|
|
||||||
"webrtc-util",
|
|
||||||
"x25519-dalek",
|
|
||||||
"x509-parser",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dtoa"
|
name = "dtoa"
|
||||||
version = "0.4.8"
|
version = "0.4.8"
|
||||||
@@ -2890,7 +2784,7 @@ dependencies = [
|
|||||||
"is-terminal",
|
"is-terminal",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"log",
|
"log",
|
||||||
"nu-ansi-term 0.49.0",
|
"nu-ansi-term",
|
||||||
"regex",
|
"regex",
|
||||||
"thiserror 1.0.61",
|
"thiserror 1.0.61",
|
||||||
]
|
]
|
||||||
@@ -3793,6 +3687,7 @@ dependencies = [
|
|||||||
"mac_address",
|
"mac_address",
|
||||||
"machine-uid",
|
"machine-uid",
|
||||||
"osascript",
|
"osascript",
|
||||||
|
"percent-encoding",
|
||||||
"protobuf",
|
"protobuf",
|
||||||
"protobuf-codegen",
|
"protobuf-codegen",
|
||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
@@ -3818,14 +3713,14 @@ dependencies = [
|
|||||||
"toml 0.7.8",
|
"toml 0.7.8",
|
||||||
"tungstenite",
|
"tungstenite",
|
||||||
"url",
|
"url",
|
||||||
"users 0.11.0",
|
"users",
|
||||||
"uuid",
|
"uuid",
|
||||||
"webpki-roots 1.0.9",
|
"webpki-roots 1.0.9",
|
||||||
"webrtc",
|
"webrtc",
|
||||||
"whoami",
|
"whoami",
|
||||||
"winapi 0.3.9",
|
"winapi 0.3.9",
|
||||||
"x11 2.21.0",
|
"x11 2.21.0",
|
||||||
"zstd 0.13.1",
|
"zstd",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4204,16 +4099,15 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "interceptor"
|
name = "interceptor"
|
||||||
version = "0.15.0"
|
version = "0.14.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ea51375727680dc15f06e8ad90fa31df75d79dd030100e8ad60eef1c27fe2c98"
|
checksum = "1ac0781c825d602095113772e389ef0607afcb869ae0e68a590d8e0799cdcef8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"bytes",
|
"bytes",
|
||||||
"futures",
|
|
||||||
"log",
|
"log",
|
||||||
"portable-atomic",
|
"portable-atomic",
|
||||||
"rand 0.9.2",
|
"rand 0.8.5",
|
||||||
"rtcp",
|
"rtcp",
|
||||||
"rtp",
|
"rtp",
|
||||||
"thiserror 1.0.61",
|
"thiserror 1.0.61",
|
||||||
@@ -4365,11 +4259,11 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "kcp-sys"
|
name = "kcp-sys"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/rustdesk-org/kcp-sys#32a6c09fc6223f54aea83981a6aa8995931d29be"
|
source = "git+https://github.com/rustdesk-org/kcp-sys?branch=rustdesk-patches#023a0065398968989f2ddfcf5cc72bb886d02675"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"auto_impl",
|
"auto_impl",
|
||||||
"bindgen 0.71.1",
|
"bindgen 0.72.1",
|
||||||
"bitflags 2.9.1",
|
"bitflags 2.9.1",
|
||||||
"bytes",
|
"bytes",
|
||||||
"cc",
|
"cc",
|
||||||
@@ -4380,8 +4274,6 @@ dependencies = [
|
|||||||
"thiserror 2.0.17",
|
"thiserror 2.0.17",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
"tracing",
|
|
||||||
"tracing-subscriber",
|
|
||||||
"zerocopy 0.7.34",
|
"zerocopy 0.7.34",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -4515,7 +4407,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "e310b3a6b5907f99202fcdb4960ff45b93735d7c7d96b760fcff8db2dc0e103d"
|
checksum = "e310b3a6b5907f99202fcdb4960ff45b93735d7c7d96b760fcff8db2dc0e103d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if 1.0.0",
|
"cfg-if 1.0.0",
|
||||||
"windows-targets 0.52.6",
|
"windows-targets 0.48.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -5237,16 +5129,6 @@ dependencies = [
|
|||||||
"winapi 0.3.9",
|
"winapi 0.3.9",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "nu-ansi-term"
|
|
||||||
version = "0.46.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84"
|
|
||||||
dependencies = [
|
|
||||||
"overload",
|
|
||||||
"winapi 0.3.9",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nu-ansi-term"
|
name = "nu-ansi-term"
|
||||||
version = "0.49.0"
|
version = "0.49.0"
|
||||||
@@ -5910,12 +5792,6 @@ dependencies = [
|
|||||||
"serde_json 1.0.118",
|
"serde_json 1.0.118",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "overload"
|
|
||||||
version = "0.1.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "owned_ttf_parser"
|
name = "owned_ttf_parser"
|
||||||
version = "0.25.1"
|
version = "0.25.1"
|
||||||
@@ -5959,37 +5835,6 @@ dependencies = [
|
|||||||
"winapi 0.3.9",
|
"winapi 0.3.9",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pam"
|
|
||||||
version = "0.7.0"
|
|
||||||
source = "git+https://github.com/rustdesk-org/pam#7bfd25510202cd269292cbdd7c71f3977a6fd762"
|
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
"pam-macros",
|
|
||||||
"pam-sys",
|
|
||||||
"users 0.10.0",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pam-macros"
|
|
||||||
version = "0.0.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "c94f3b9b97df3c6d4e51a14916639b24e02c7d15d1dba686ce9b1118277cb811"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2 1.0.93",
|
|
||||||
"quote 1.0.36",
|
|
||||||
"syn 1.0.109",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pam-sys"
|
|
||||||
version = "1.0.0-alpha4"
|
|
||||||
source = "git+https://github.com/rustdesk-org/pam-sys?branch=fix/v1.0.0-alpha4_gnuc_va_list#3337c9bb9a9c68d7497ec8c93cad2368c26091b7"
|
|
||||||
dependencies = [
|
|
||||||
"bindgen 0.59.2",
|
|
||||||
"libc",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pango"
|
name = "pango"
|
||||||
version = "0.18.3"
|
version = "0.18.3"
|
||||||
@@ -6057,35 +5902,12 @@ dependencies = [
|
|||||||
"windows-targets 0.52.6",
|
"windows-targets 0.52.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "password-hash"
|
|
||||||
version = "0.4.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "7676374caaee8a325c9e7a2ae557f216c5563a171d6997b0ef8a65af35147700"
|
|
||||||
dependencies = [
|
|
||||||
"base64ct",
|
|
||||||
"rand_core 0.6.4",
|
|
||||||
"subtle",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "paste"
|
name = "paste"
|
||||||
version = "1.0.15"
|
version = "1.0.15"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
|
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pbkdf2"
|
|
||||||
version = "0.11.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917"
|
|
||||||
dependencies = [
|
|
||||||
"digest",
|
|
||||||
"hmac",
|
|
||||||
"password-hash",
|
|
||||||
"sha2",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "peeking_take_while"
|
name = "peeking_take_while"
|
||||||
version = "0.1.2"
|
version = "0.1.2"
|
||||||
@@ -6358,17 +6180,6 @@ dependencies = [
|
|||||||
"windows-sys 0.52.0",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "poly1305"
|
|
||||||
version = "0.8.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf"
|
|
||||||
dependencies = [
|
|
||||||
"cpufeatures",
|
|
||||||
"opaque-debug",
|
|
||||||
"universal-hash",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "polyval"
|
name = "polyval"
|
||||||
version = "0.6.2"
|
version = "0.6.2"
|
||||||
@@ -7175,9 +6986,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rtcp"
|
name = "rtcp"
|
||||||
version = "0.14.0"
|
version = "0.13.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "81d30d1c4091644431c22acf9f8be6191b56805e0e977f15ca7104b4a6d6eaec"
|
checksum = "e9689528bf3a9eb311fd938d05516dd546412f9ce4fffc8acfc1db27cc3dbf72"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"thiserror 1.0.61",
|
"thiserror 1.0.61",
|
||||||
@@ -7186,14 +6997,14 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rtp"
|
name = "rtp"
|
||||||
version = "0.14.0"
|
version = "0.13.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2f126f38ea84c02480e32e547c1459a939052f74fb92117ac3eef23fdac6b023"
|
checksum = "c54733451a67d76caf9caa07a7a2cec6871ea9dda92a7847f98063d459200f4b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"memchr",
|
"memchr",
|
||||||
"portable-atomic",
|
"portable-atomic",
|
||||||
"rand 0.9.2",
|
"rand 0.8.5",
|
||||||
"serde 1.0.228",
|
"serde 1.0.228",
|
||||||
"thiserror 1.0.61",
|
"thiserror 1.0.61",
|
||||||
"webrtc-util",
|
"webrtc-util",
|
||||||
@@ -7258,7 +7069,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustdesk"
|
name = "rustdesk"
|
||||||
version = "1.4.9"
|
version = "1.5.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"android-wakelock",
|
"android-wakelock",
|
||||||
"android_logger",
|
"android_logger",
|
||||||
@@ -7316,7 +7127,6 @@ dependencies = [
|
|||||||
"once_cell",
|
"once_cell",
|
||||||
"openssl",
|
"openssl",
|
||||||
"os-version",
|
"os-version",
|
||||||
"pam",
|
|
||||||
"parity-tokio-ipc",
|
"parity-tokio-ipc",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"piet",
|
"piet",
|
||||||
@@ -7349,6 +7159,7 @@ dependencies = [
|
|||||||
"terminfo",
|
"terminfo",
|
||||||
"termios 0.3.3",
|
"termios 0.3.3",
|
||||||
"tiny-skia",
|
"tiny-skia",
|
||||||
|
"tokio",
|
||||||
"totp-rs",
|
"totp-rs",
|
||||||
"tray-icon",
|
"tray-icon",
|
||||||
"ttf-parser",
|
"ttf-parser",
|
||||||
@@ -7365,12 +7176,11 @@ dependencies = [
|
|||||||
"wol-rs",
|
"wol-rs",
|
||||||
"x11-clipboard 0.8.1",
|
"x11-clipboard 0.8.1",
|
||||||
"x11rb 0.12.0",
|
"x11rb 0.12.0",
|
||||||
"zip",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustdesk-portable-packer"
|
name = "rustdesk-portable-packer"
|
||||||
version = "1.4.9"
|
version = "1.5.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"brotli",
|
"brotli",
|
||||||
"dirs 5.0.1",
|
"dirs 5.0.1",
|
||||||
@@ -7630,11 +7440,11 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sdp"
|
name = "sdp"
|
||||||
version = "0.10.0"
|
version = "0.8.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "32c374dceda16965d541c8800ce9cc4e1c14acfd661ddf7952feeedc3411e5c6"
|
checksum = "4cd277015eada44a0bb810a4b84d3bf6e810573fa62fb442f457edf6a1087a69"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"rand 0.9.2",
|
"rand 0.8.5",
|
||||||
"substring",
|
"substring",
|
||||||
"thiserror 1.0.61",
|
"thiserror 1.0.61",
|
||||||
"url",
|
"url",
|
||||||
@@ -7864,15 +7674,6 @@ dependencies = [
|
|||||||
"tzdb 0.5.10",
|
"tzdb 0.5.10",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "sharded-slab"
|
|
||||||
version = "0.1.7"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
|
|
||||||
dependencies = [
|
|
||||||
"lazy_static",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "shared_library"
|
name = "shared_library"
|
||||||
version = "0.1.9"
|
version = "0.1.9"
|
||||||
@@ -8212,15 +8013,15 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "stun"
|
name = "stun"
|
||||||
version = "0.9.0"
|
version = "0.8.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1a512c5d501e3e3b5a4bb3e8e31462d56d54a66b95a28b8596e14422bf21c32b"
|
checksum = "7dbc2bab375524093c143dc362a03fb6a1fb79e938391cdb21665688f88a088a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"crc",
|
"crc",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"md-5",
|
"md-5",
|
||||||
"rand 0.9.2",
|
"rand 0.8.5",
|
||||||
"ring",
|
"ring",
|
||||||
"subtle",
|
"subtle",
|
||||||
"thiserror 1.0.61",
|
"thiserror 1.0.61",
|
||||||
@@ -8602,16 +8403,6 @@ dependencies = [
|
|||||||
"syn 2.0.98",
|
"syn 2.0.98",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "thread_local"
|
|
||||||
version = "1.1.8"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c"
|
|
||||||
dependencies = [
|
|
||||||
"cfg-if 1.0.0",
|
|
||||||
"once_cell",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "threadpool"
|
name = "threadpool"
|
||||||
version = "1.8.1"
|
version = "1.8.1"
|
||||||
@@ -8907,7 +8698,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "6c4ae9724c5888c0417d2396037ed3b60665925624766416e3e342b6ba5dbd3f"
|
checksum = "6c4ae9724c5888c0417d2396037ed3b60665925624766416e3e342b6ba5dbd3f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base32",
|
"base32",
|
||||||
"constant_time_eq 0.2.6",
|
"constant_time_eq",
|
||||||
"hmac",
|
"hmac",
|
||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
"sha1",
|
"sha1",
|
||||||
@@ -8991,32 +8782,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678"
|
checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"valuable",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "tracing-log"
|
|
||||||
version = "0.2.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
|
|
||||||
dependencies = [
|
|
||||||
"log",
|
|
||||||
"once_cell",
|
|
||||||
"tracing-core",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "tracing-subscriber"
|
|
||||||
version = "0.3.19"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008"
|
|
||||||
dependencies = [
|
|
||||||
"nu-ansi-term 0.46.0",
|
|
||||||
"sharded-slab",
|
|
||||||
"smallvec",
|
|
||||||
"thread_local",
|
|
||||||
"tracing-core",
|
|
||||||
"tracing-log",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -9131,9 +8896,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "turn"
|
name = "turn"
|
||||||
version = "0.11.0"
|
version = "0.10.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5ed995882f66ab94238de77c62e5e778389698ab700afa4696f4754da8f457cb"
|
checksum = "3f5aea1116456e1da71c45586b87c72e3b43164fbf435eb93ff6aa475416a9a4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
@@ -9141,7 +8906,7 @@ dependencies = [
|
|||||||
"log",
|
"log",
|
||||||
"md-5",
|
"md-5",
|
||||||
"portable-atomic",
|
"portable-atomic",
|
||||||
"rand 0.9.2",
|
"rand 0.8.5",
|
||||||
"ring",
|
"ring",
|
||||||
"stun",
|
"stun",
|
||||||
"thiserror 1.0.61",
|
"thiserror 1.0.61",
|
||||||
@@ -9267,12 +9032,6 @@ dependencies = [
|
|||||||
"unic-common",
|
"unic-common",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "unicase"
|
|
||||||
version = "2.8.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-bidi"
|
name = "unicode-bidi"
|
||||||
version = "0.3.15"
|
version = "0.3.15"
|
||||||
@@ -9352,16 +9111,6 @@ version = "2.1.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
|
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "users"
|
|
||||||
version = "0.10.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "aa4227e95324a443c9fcb06e03d4d85e91aabe9a5a02aa818688b6918b6af486"
|
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
"log",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "users"
|
name = "users"
|
||||||
version = "0.11.0"
|
version = "0.11.0"
|
||||||
@@ -9428,12 +9177,6 @@ dependencies = [
|
|||||||
"bindgen 0.65.1",
|
"bindgen 0.65.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "valuable"
|
|
||||||
version = "0.1.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "vcpkg"
|
name = "vcpkg"
|
||||||
version = "0.2.15"
|
version = "0.2.15"
|
||||||
@@ -9817,25 +9560,26 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "webrtc"
|
name = "webrtc"
|
||||||
version = "0.14.0"
|
version = "0.13.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "08fd686c0920ac08f3a57eacc48e31f0e4ca1ffefba4478784606f78c14e83ad"
|
checksum = "24bab7195998d605c862772f90a452ba655b90a2f463c850ac032038890e367a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arc-swap",
|
"arc-swap",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"bytes",
|
"bytes",
|
||||||
"dtls",
|
"cfg-if 1.0.0",
|
||||||
"hex",
|
"hex",
|
||||||
"interceptor",
|
"interceptor",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"log",
|
"log",
|
||||||
"portable-atomic",
|
"portable-atomic",
|
||||||
"rand 0.9.2",
|
"rand 0.8.5",
|
||||||
"rcgen",
|
"rcgen",
|
||||||
"regex",
|
"regex",
|
||||||
"ring",
|
"ring",
|
||||||
"rtcp",
|
"rtcp",
|
||||||
"rtp",
|
"rtp",
|
||||||
|
"rustls",
|
||||||
"sdp",
|
"sdp",
|
||||||
"serde 1.0.228",
|
"serde 1.0.228",
|
||||||
"serde_json 1.0.118",
|
"serde_json 1.0.118",
|
||||||
@@ -9843,12 +9587,13 @@ dependencies = [
|
|||||||
"smol_str",
|
"smol_str",
|
||||||
"stun",
|
"stun",
|
||||||
"thiserror 1.0.61",
|
"thiserror 1.0.61",
|
||||||
|
"time 0.3.36",
|
||||||
"tokio",
|
"tokio",
|
||||||
"turn",
|
"turn",
|
||||||
"unicase",
|
|
||||||
"url",
|
"url",
|
||||||
"waitgroup",
|
"waitgroup",
|
||||||
"webrtc-data",
|
"webrtc-data",
|
||||||
|
"webrtc-dtls",
|
||||||
"webrtc-ice",
|
"webrtc-ice",
|
||||||
"webrtc-mdns",
|
"webrtc-mdns",
|
||||||
"webrtc-media",
|
"webrtc-media",
|
||||||
@@ -9859,9 +9604,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "webrtc-data"
|
name = "webrtc-data"
|
||||||
version = "0.12.0"
|
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 = "062a5438d63bb0756a221693d76cc0dd6119affee1dfdfe57abe3a2a8c8b3eea"
|
checksum = "4e97b932854da633a767eff0cc805425a2222fc6481e96f463e57b015d949d1d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"log",
|
"log",
|
||||||
@@ -9873,17 +9618,54 @@ dependencies = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "webrtc-ice"
|
name = "webrtc-dtls"
|
||||||
version = "0.14.0"
|
version = "0.12.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "69cb13fd1a373e68addc4bba0c8ca058627518e54342583d024bdcbb8ae5d97d"
|
checksum = "5ccbe4d9049390ab52695c3646c1395c877e16c15fb05d3bda8eee0c7351711c"
|
||||||
|
dependencies = [
|
||||||
|
"aes",
|
||||||
|
"aes-gcm",
|
||||||
|
"async-trait",
|
||||||
|
"bincode",
|
||||||
|
"byteorder",
|
||||||
|
"cbc",
|
||||||
|
"ccm",
|
||||||
|
"der-parser",
|
||||||
|
"hkdf",
|
||||||
|
"hmac",
|
||||||
|
"log",
|
||||||
|
"p256",
|
||||||
|
"p384",
|
||||||
|
"portable-atomic",
|
||||||
|
"rand 0.8.5",
|
||||||
|
"rand_core 0.6.4",
|
||||||
|
"rcgen",
|
||||||
|
"ring",
|
||||||
|
"rustls",
|
||||||
|
"sec1",
|
||||||
|
"serde 1.0.228",
|
||||||
|
"sha1",
|
||||||
|
"sha2",
|
||||||
|
"subtle",
|
||||||
|
"thiserror 1.0.61",
|
||||||
|
"tokio",
|
||||||
|
"webrtc-util",
|
||||||
|
"x25519-dalek",
|
||||||
|
"x509-parser",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "webrtc-ice"
|
||||||
|
version = "0.13.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "eb51bde0d790f109a15bfe4d04f1b56fb51d567da231643cb3f21bb74d678997"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arc-swap",
|
"arc-swap",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"crc",
|
"crc",
|
||||||
"log",
|
"log",
|
||||||
"portable-atomic",
|
"portable-atomic",
|
||||||
"rand 0.9.2",
|
"rand 0.8.5",
|
||||||
"serde 1.0.228",
|
"serde 1.0.228",
|
||||||
"serde_json 1.0.118",
|
"serde_json 1.0.118",
|
||||||
"stun",
|
"stun",
|
||||||
@@ -9899,9 +9681,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "webrtc-mdns"
|
name = "webrtc-mdns"
|
||||||
version = "0.10.0"
|
version = "0.9.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a17279a067e75df72ce923fdeb7f04cd808f6f5aa4910dc6bcb4fbe66b396ace"
|
checksum = "979cc85259c53b7b620803509d10d35e2546fa505d228850cbe3f08765ea6ea8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"log",
|
"log",
|
||||||
"socket2 0.5.10",
|
"socket2 0.5.10",
|
||||||
@@ -9912,22 +9694,21 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "webrtc-media"
|
name = "webrtc-media"
|
||||||
version = "0.11.0"
|
version = "0.10.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "94a84c910fec0848fd5a0d8a5651e0ddbdedaf25a7d3ae3f0b15f71ac73a1773"
|
checksum = "80041211deccda758a3e19aa93d6b10bc1d37c9183b519054b40a83691d13810"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"byteorder",
|
"byteorder",
|
||||||
"bytes",
|
"bytes",
|
||||||
"rand 0.9.2",
|
"rand 0.8.5",
|
||||||
"rtp",
|
"rtp",
|
||||||
"thiserror 1.0.61",
|
"thiserror 1.0.61",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "webrtc-sctp"
|
name = "webrtc-sctp"
|
||||||
version = "0.13.0"
|
version = "0.12.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "git+https://github.com/rustdesk-org/webrtc?rev=48100bf13e694d7e5bbb49b9a753dbad1c359d0c#48100bf13e694d7e5bbb49b9a753dbad1c359d0c"
|
||||||
checksum = "f985465467d8910c1f8ac4382cd64f83b1f6a1a75021a82b221546f6fb3b856f"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arc-swap",
|
"arc-swap",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
@@ -9935,7 +9716,7 @@ dependencies = [
|
|||||||
"crc",
|
"crc",
|
||||||
"log",
|
"log",
|
||||||
"portable-atomic",
|
"portable-atomic",
|
||||||
"rand 0.9.2",
|
"rand 0.8.5",
|
||||||
"thiserror 1.0.61",
|
"thiserror 1.0.61",
|
||||||
"tokio",
|
"tokio",
|
||||||
"webrtc-util",
|
"webrtc-util",
|
||||||
@@ -9943,9 +9724,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "webrtc-srtp"
|
name = "webrtc-srtp"
|
||||||
version = "0.16.0"
|
version = "0.15.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "66d8cdc33413f1d0192670a80ce93d17cb78d57fe3a2414be30d6f6dff121123"
|
checksum = "01e773f79b09b057ffbda6b03fe7b43403b012a240cf8d05d630674c3723b5bb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aead",
|
"aead",
|
||||||
"aes",
|
"aes",
|
||||||
@@ -9966,19 +9747,19 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "webrtc-util"
|
name = "webrtc-util"
|
||||||
version = "0.12.0"
|
version = "0.11.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "git+https://github.com/rustdesk-org/webrtc?rev=48100bf13e694d7e5bbb49b9a753dbad1c359d0c#48100bf13e694d7e5bbb49b9a753dbad1c359d0c"
|
||||||
checksum = "d1c0c7e0c8f280f2bbfae442701465777ac07adaf46ce0c5863cd58e13fe472a"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"bitflags 1.3.2",
|
"bitflags 1.3.2",
|
||||||
"bytes",
|
"bytes",
|
||||||
"ipnet",
|
"ipnet",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
|
"libc",
|
||||||
"log",
|
"log",
|
||||||
"nix 0.26.4",
|
"nix 0.26.4",
|
||||||
"portable-atomic",
|
"portable-atomic",
|
||||||
"rand 0.9.2",
|
"rand 0.8.5",
|
||||||
"thiserror 1.0.61",
|
"thiserror 1.0.61",
|
||||||
"tokio",
|
"tokio",
|
||||||
"winapi 0.3.9",
|
"winapi 0.3.9",
|
||||||
@@ -11157,52 +10938,13 @@ dependencies = [
|
|||||||
"syn 2.0.98",
|
"syn 2.0.98",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "zip"
|
|
||||||
version = "0.6.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261"
|
|
||||||
dependencies = [
|
|
||||||
"aes",
|
|
||||||
"byteorder",
|
|
||||||
"bzip2",
|
|
||||||
"constant_time_eq 0.1.5",
|
|
||||||
"crc32fast",
|
|
||||||
"crossbeam-utils",
|
|
||||||
"flate2",
|
|
||||||
"hmac",
|
|
||||||
"pbkdf2",
|
|
||||||
"sha1",
|
|
||||||
"time 0.3.36",
|
|
||||||
"zstd 0.11.2+zstd.1.5.2",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "zstd"
|
|
||||||
version = "0.11.2+zstd.1.5.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "20cc960326ece64f010d2d2107537f26dc589a6573a316bd5b1dba685fa5fde4"
|
|
||||||
dependencies = [
|
|
||||||
"zstd-safe 5.0.2+zstd.1.5.2",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zstd"
|
name = "zstd"
|
||||||
version = "0.13.1"
|
version = "0.13.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2d789b1514203a1120ad2429eae43a7bd32b90976a7bb8a05f7ec02fa88cc23a"
|
checksum = "2d789b1514203a1120ad2429eae43a7bd32b90976a7bb8a05f7ec02fa88cc23a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"zstd-safe 7.1.0",
|
"zstd-safe",
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "zstd-safe"
|
|
||||||
version = "5.0.2+zstd.1.5.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "1d2a5585e04f9eea4b2a3d1eca508c4dee9592a89ef6f450c11719da0726f4db"
|
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
"zstd-sys",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
25
Cargo.toml
25
Cargo.toml
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "rustdesk"
|
name = "rustdesk"
|
||||||
version = "1.4.9"
|
version = "1.5.0"
|
||||||
authors = ["rustdesk <info@rustdesk.com>"]
|
authors = ["rustdesk <info@rustdesk.com>"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
build= "build.rs"
|
build= "build.rs"
|
||||||
@@ -37,7 +37,6 @@ drm = ["scrap/drm"]
|
|||||||
# kind of operation and deserves a switch that can remove it from the binary entirely, without
|
# kind of operation and deserves a switch that can remove it from the binary entirely, without
|
||||||
# giving up DRM capture: `--features drm` builds the capture path with no wake code compiled in.
|
# giving up DRM capture: `--features drm` builds the capture path with no wake code compiled in.
|
||||||
drm-wake = ["drm"]
|
drm-wake = ["drm"]
|
||||||
plugin_framework = []
|
|
||||||
linux-pkg-config = ["magnum-opus/linux-pkg-config", "scrap/linux-pkg-config"]
|
linux-pkg-config = ["magnum-opus/linux-pkg-config", "scrap/linux-pkg-config"]
|
||||||
unix-file-copy-paste = [
|
unix-file-copy-paste = [
|
||||||
"dep:x11-clipboard",
|
"dep:x11-clipboard",
|
||||||
@@ -53,7 +52,7 @@ screencapturekit = ["cpal/screencapturekit"]
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
async-trait = "0.1"
|
async-trait = "0.1"
|
||||||
scrap = { path = "libs/scrap", features = ["wayland"] }
|
scrap = { path = "libs/scrap", features = ["wayland"] }
|
||||||
hbb_common = { path = "libs/hbb_common" }
|
hbb_common = { path = "libs/hbb_common", features = ["webrtc"] }
|
||||||
serde_derive = "1.0"
|
serde_derive = "1.0"
|
||||||
serde = "1.0"
|
serde = "1.0"
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
@@ -81,11 +80,10 @@ hex = "0.4"
|
|||||||
chrono = "0.4"
|
chrono = "0.4"
|
||||||
cidr-utils = "0.5"
|
cidr-utils = "0.5"
|
||||||
fon = "0.6"
|
fon = "0.6"
|
||||||
zip = "0.6"
|
|
||||||
shutdown_hooks = "0.1"
|
shutdown_hooks = "0.1"
|
||||||
totp-rs = { version = "5.4", default-features = false, features = ["gen_secret", "otpauth"] }
|
totp-rs = { version = "5.4", default-features = false, features = ["gen_secret", "otpauth"] }
|
||||||
stunclient = "0.4"
|
stunclient = "0.4"
|
||||||
kcp-sys= { git = "https://github.com/rustdesk-org/kcp-sys"}
|
kcp-sys= { git = "https://github.com/rustdesk-org/kcp-sys", branch = "rustdesk-patches" }
|
||||||
reqwest = { version = "0.12", features = ["blocking", "socks", "json", "native-tls", "rustls-tls", "rustls-tls-native-roots", "gzip", "zstd"], default-features=false }
|
reqwest = { version = "0.12", features = ["blocking", "socks", "json", "native-tls", "rustls-tls", "rustls-tls-native-roots", "gzip", "zstd"], default-features=false }
|
||||||
|
|
||||||
[target.'cfg(not(target_os = "linux"))'.dependencies]
|
[target.'cfg(not(target_os = "linux"))'.dependencies]
|
||||||
@@ -191,7 +189,6 @@ async-process = "1.7"
|
|||||||
evdev = { git="https://github.com/rustdesk-org/evdev" }
|
evdev = { git="https://github.com/rustdesk-org/evdev" }
|
||||||
dbus = "0.9"
|
dbus = "0.9"
|
||||||
dbus-crossroads = "0.5"
|
dbus-crossroads = "0.5"
|
||||||
pam = { git="https://github.com/rustdesk-org/pam" }
|
|
||||||
x11-clipboard = {git="https://github.com/clslaid/x11-clipboard", branch = "feat/store-batch", optional = true}
|
x11-clipboard = {git="https://github.com/clslaid/x11-clipboard", branch = "feat/store-batch", optional = true}
|
||||||
x11rb = {version = "0.12", features = ["all-extensions"], optional = true}
|
x11rb = {version = "0.12", features = ["all-extensions"], optional = true}
|
||||||
percent-encoding = {version = "2.3", optional = true}
|
percent-encoding = {version = "2.3", optional = true}
|
||||||
@@ -212,12 +209,25 @@ android-wakelock = { git = "https://github.com/rustdesk-org/android-wakelock" }
|
|||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
members = ["libs/scrap", "libs/hbb_common", "libs/enigo", "libs/clipboard", "libs/virtual_display", "libs/virtual_display/dylib", "libs/portable", "libs/remote_printer"]
|
members = ["libs/scrap", "libs/hbb_common", "libs/enigo", "libs/clipboard", "libs/virtual_display", "libs/virtual_display/dylib", "libs/portable", "libs/remote_printer"]
|
||||||
exclude = ["vdi/host", "examples/custom_plugin"]
|
exclude = ["vdi/host"]
|
||||||
|
|
||||||
# Patch libxdo-sys to use a stub implementation that doesn't require libxdo
|
# Patch libxdo-sys to use a stub implementation that doesn't require libxdo
|
||||||
# This allows building and running on systems without libxdo installed (e.g., Wayland-only)
|
# This allows building and running on systems without libxdo installed (e.g., Wayland-only)
|
||||||
[patch.crates-io]
|
[patch.crates-io]
|
||||||
libxdo-sys = { path = "libs/libxdo-sys-stub" }
|
libxdo-sys = { path = "libs/libxdo-sys-stub" }
|
||||||
|
# One branch off upstream v0.13.0, the tag whose crate versions match this stack.
|
||||||
|
# webrtc-util: reads the Windows adapter list's IPv6 addresses as host-order u16 groups, so every
|
||||||
|
# one comes out byte-swapped, fails to bind, and ICE gathers no IPv6 host candidate on Windows.
|
||||||
|
# webrtc-sctp: RFC 4960's 1s RTO floor makes a single loss cost 1-3s on a link whose RTT is 24-64ms,
|
||||||
|
# and fast retransmit cannot cover a request/response exchange; INITIAL_MTU 1228 also fragments on
|
||||||
|
# IPv6; and its AIMD pins a lossy long-haul link to MSS/(RTT*sqrt(p)), so a switch sends without
|
||||||
|
# a congestion window, as KCP does - on by default, `allow-webrtc-congestion-control` opts back in.
|
||||||
|
# Sending that way, a reordering window keeps a chunk that is merely late from being resent on a
|
||||||
|
# path that jitters, every DATA chunk asks for its SACK at once so a lost tail is back within an
|
||||||
|
# RTT at KCP's RTO floors, and bundles of small chunks stay within the MTU.
|
||||||
|
# Pinned by rev, not branch: a fork branch can be rewritten out from under the lockfile.
|
||||||
|
webrtc-util = { git = "https://github.com/rustdesk-org/webrtc", rev = "48100bf13e694d7e5bbb49b9a753dbad1c359d0c" }
|
||||||
|
webrtc-sctp = { git = "https://github.com/rustdesk-org/webrtc", rev = "48100bf13e694d7e5bbb49b9a753dbad1c359d0c" }
|
||||||
|
|
||||||
[package.metadata.winres]
|
[package.metadata.winres]
|
||||||
LegalCopyright = "Copyright © 2026 Purslane Tech Pte. Ltd. All rights reserved."
|
LegalCopyright = "Copyright © 2026 Purslane Tech Pte. Ltd. All rights reserved."
|
||||||
@@ -237,6 +247,7 @@ os-version = "0.2"
|
|||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
hound = "3.5"
|
hound = "3.5"
|
||||||
docopt = "1.1"
|
docopt = "1.1"
|
||||||
|
tokio = { version = "1.44", features = ["test-util"] }
|
||||||
|
|
||||||
[package.metadata.bundle]
|
[package.metadata.bundle]
|
||||||
name = "RustDesk"
|
name = "RustDesk"
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ 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 \
|
||||||
wget \
|
wget \
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<a href="#raw-steps-to-build">Build</a> •
|
<a href="#raw-steps-to-build">Build</a> •
|
||||||
<a href="#how-to-build-with-docker">Docker</a> •
|
<a href="#how-to-build-with-docker">Docker</a> •
|
||||||
<a href="#file-structure">Structure</a> •
|
<a href="#file-structure">Structure</a> •
|
||||||
<a href="#snapshot">Snapshot</a><br>
|
<a href="#screenshots">Screenshots</a><br>
|
||||||
[<a href="docs/README-UA.md">Українська</a>] | [<a href="docs/README-CS.md">česky</a>] | [<a href="docs/README-ZH.md">中文</a>] | [<a href="docs/README-HU.md">Magyar</a>] | [<a href="docs/README-ES.md">Español</a>] | [<a href="docs/README-FA.md">فارسی</a>] | [<a href="docs/README-FR.md">Français</a>] | [<a href="docs/README-DE.md">Deutsch</a>] | [<a href="docs/README-PL.md">Polski</a>] | [<a href="docs/README-ID.md">Indonesian</a>] | [<a href="docs/README-FI.md">Suomi</a>] | [<a href="docs/README-ML.md">മലയാളം</a>] | [<a href="docs/README-JP.md">日本語</a>] | [<a href="docs/README-NL.md">Nederlands</a>] | [<a href="docs/README-IT.md">Italiano</a>] | [<a href="docs/README-RU.md">Русский</a>] | [<a href="docs/README-PTBR.md">Português (Brasil)</a>] | [<a href="docs/README-EO.md">Esperanto</a>] | [<a href="docs/README-KR.md">한국어</a>] | [<a href="docs/README-AR.md">العربي</a>] | [<a href="docs/README-VN.md">Tiếng Việt</a>] | [<a href="docs/README-DA.md">Dansk</a>] | [<a href="docs/README-GR.md">Ελληνικά</a>] | [<a href="docs/README-TR.md">Türkçe</a>] | [<a href="docs/README-NO.md">Norsk</a>] | [<a href="docs/README-RO.md">Română</a>]<br>
|
[<a href="docs/README-UA.md">Українська</a>] | [<a href="docs/README-CS.md">česky</a>] | [<a href="docs/README-ZH.md">中文</a>] | [<a href="docs/README-HU.md">Magyar</a>] | [<a href="docs/README-ES.md">Español</a>] | [<a href="docs/README-FA.md">فارسی</a>] | [<a href="docs/README-FR.md">Français</a>] | [<a href="docs/README-DE.md">Deutsch</a>] | [<a href="docs/README-PL.md">Polski</a>] | [<a href="docs/README-ID.md">Indonesian</a>] | [<a href="docs/README-FI.md">Suomi</a>] | [<a href="docs/README-ML.md">മലയാളം</a>] | [<a href="docs/README-JP.md">日本語</a>] | [<a href="docs/README-NL.md">Nederlands</a>] | [<a href="docs/README-IT.md">Italiano</a>] | [<a href="docs/README-RU.md">Русский</a>] | [<a href="docs/README-PTBR.md">Português (Brasil)</a>] | [<a href="docs/README-EO.md">Esperanto</a>] | [<a href="docs/README-KR.md">한국어</a>] | [<a href="docs/README-AR.md">العربي</a>] | [<a href="docs/README-VN.md">Tiếng Việt</a>] | [<a href="docs/README-DA.md">Dansk</a>] | [<a href="docs/README-GR.md">Ελληνικά</a>] | [<a href="docs/README-TR.md">Türkçe</a>] | [<a href="docs/README-NO.md">Norsk</a>] | [<a href="docs/README-RO.md">Română</a>]<br>
|
||||||
<b>We need your help to translate this README, <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">RustDesk UI</a> and <a href="https://github.com/rustdesk/doc.rustdesk.com">RustDesk Doc</a> to your native language</b>
|
<b>We need your help to translate this README, <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">RustDesk UI</a> and <a href="https://github.com/rustdesk/doc.rustdesk.com">RustDesk Doc</a> to your native language</b>
|
||||||
</p>
|
</p>
|
||||||
@@ -66,19 +66,19 @@ Please download Sciter dynamic library yourself.
|
|||||||
```sh
|
```sh
|
||||||
sudo apt install -y zip g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev \
|
sudo apt install -y zip g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev \
|
||||||
libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake make \
|
libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake make \
|
||||||
libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libpam0g-dev
|
libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev
|
||||||
```
|
```
|
||||||
|
|
||||||
### openSUSE Tumbleweed
|
### openSUSE Tumbleweed
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel pam-devel
|
sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel
|
||||||
```
|
```
|
||||||
|
|
||||||
### Fedora 28 (CentOS 8)
|
### Fedora 28 (CentOS 8)
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel gstreamer1-devel gstreamer1-plugins-base-devel pam-devel
|
sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel gstreamer1-devel gstreamer1-plugins-base-devel
|
||||||
```
|
```
|
||||||
|
|
||||||
### Arch (Manjaro)
|
### Arch (Manjaro)
|
||||||
@@ -168,7 +168,6 @@ Please ensure that you run these commands from the root of the RustDesk reposito
|
|||||||
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Communicate with [rustdesk-server](https://github.com/rustdesk/rustdesk-server), wait for remote direct (TCP hole punching) or relayed connection
|
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Communicate with [rustdesk-server](https://github.com/rustdesk/rustdesk-server), wait for remote direct (TCP hole punching) or relayed connection
|
||||||
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: platform specific code
|
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: platform specific code
|
||||||
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: Flutter code for desktop and mobile
|
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: Flutter code for desktop and mobile
|
||||||
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/v1/js)**: JavaScript for Flutter web client
|
|
||||||
|
|
||||||
## Screenshots
|
## Screenshots
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ AppDir:
|
|||||||
id: rustdesk
|
id: rustdesk
|
||||||
name: rustdesk
|
name: rustdesk
|
||||||
icon: rustdesk
|
icon: rustdesk
|
||||||
version: 1.4.9
|
version: 1.5.0
|
||||||
exec: usr/share/rustdesk/rustdesk
|
exec: usr/share/rustdesk/rustdesk
|
||||||
exec_args: $@
|
exec_args: $@
|
||||||
apt:
|
apt:
|
||||||
@@ -58,7 +58,6 @@ AppDir:
|
|||||||
- libpulse0
|
- libpulse0
|
||||||
- packagekit-gtk3-module
|
- packagekit-gtk3-module
|
||||||
- libcanberra-gtk3-module
|
- libcanberra-gtk3-module
|
||||||
- libpam0g
|
|
||||||
- libdrm2
|
- libdrm2
|
||||||
exclude:
|
exclude:
|
||||||
- humanity-icon-theme
|
- humanity-icon-theme
|
||||||
@@ -77,6 +76,13 @@ AppDir:
|
|||||||
env:
|
env:
|
||||||
GIO_MODULE_DIR: /lib64/gio/modules:/usr/lib/aarch64-linux-gnu/gio/modules:$APPDIR/usr/lib/aarch64-linux-gnu/gio/modules
|
GIO_MODULE_DIR: /lib64/gio/modules:/usr/lib/aarch64-linux-gnu/gio/modules:$APPDIR/usr/lib/aarch64-linux-gnu/gio/modules
|
||||||
GDK_BACKEND: x11
|
GDK_BACKEND: x11
|
||||||
|
# AppRun sets these to "$APPDIR/...:$XDG_DATA_DIRS", and setting them at all suppresses the XDG
|
||||||
|
# defaults, so a host that leaves them unset loses /usr/share and /etc/xdg. gdk-pixbuf 2.43+
|
||||||
|
# (Arch, Fedora) then finds no glycin loaders and every PNG decode fails, aborting on the first
|
||||||
|
# remote cursor. The host value goes last: unset it expands to an empty element, which GLib
|
||||||
|
# resolves against the CWD, and that must not outrank the defaults below.
|
||||||
|
XDG_DATA_DIRS: $APPDIR/usr/local/share:$APPDIR/usr/share:/usr/local/share:/usr/share:$XDG_DATA_DIRS
|
||||||
|
XDG_CONFIG_DIRS: $APPDIR/etc/xdg:/etc/xdg:$XDG_CONFIG_DIRS
|
||||||
APPDIR_LIBRARY_PATH: /lib64:/usr/lib/aarch64-linux-gnu:$APPDIR/lib/aarch64-linux-gnu:$APPDIR/lib/aarch64-linux-gnu/security:$APPDIR/lib/systemd:$APPDIR/usr/lib/aarch64-linux-gnu:$APPDIR/usr/lib/aarch64-linux-gnu/gdk-pixbuf-2.0/2.10.0/loaders:$APPDIR/usr/lib/aarch64-linux-gnu/gstreamer-1.0:$APPDIR/usr/lib/aarch64-linux-gnu/gtk-3.0/3.0.0/immodules:$APPDIR/usr/lib/aarch64-linux-gnu/gtk-3.0/3.0.0/printbackends:$APPDIR/usr/lib/aarch64-linux-gnu/krb5/plugins/preauth:$APPDIR/usr/lib/aarch64-linux-gnu/libcanberra-0.30:$APPDIR/usr/lib/aarch64-linux-gnu/pulseaudio:$APPDIR/usr/lib/aarch64-linux-gnu/sasl2:$APPDIR/usr/lib/aarch64-linux-gnu/vdpau:$APPDIR/usr/share/rustdesk/lib:$APPDIR/lib/aarch64
|
APPDIR_LIBRARY_PATH: /lib64:/usr/lib/aarch64-linux-gnu:$APPDIR/lib/aarch64-linux-gnu:$APPDIR/lib/aarch64-linux-gnu/security:$APPDIR/lib/systemd:$APPDIR/usr/lib/aarch64-linux-gnu:$APPDIR/usr/lib/aarch64-linux-gnu/gdk-pixbuf-2.0/2.10.0/loaders:$APPDIR/usr/lib/aarch64-linux-gnu/gstreamer-1.0:$APPDIR/usr/lib/aarch64-linux-gnu/gtk-3.0/3.0.0/immodules:$APPDIR/usr/lib/aarch64-linux-gnu/gtk-3.0/3.0.0/printbackends:$APPDIR/usr/lib/aarch64-linux-gnu/krb5/plugins/preauth:$APPDIR/usr/lib/aarch64-linux-gnu/libcanberra-0.30:$APPDIR/usr/lib/aarch64-linux-gnu/pulseaudio:$APPDIR/usr/lib/aarch64-linux-gnu/sasl2:$APPDIR/usr/lib/aarch64-linux-gnu/vdpau:$APPDIR/usr/share/rustdesk/lib:$APPDIR/lib/aarch64
|
||||||
GST_PLUGIN_PATH: /lib64/gstreamer-1.0:/usr/lib/aarch64-linux-gnu/gstreamer-1.0:$APPDIR/usr/lib/aarch64-linux-gnu/gstreamer-1.0
|
GST_PLUGIN_PATH: /lib64/gstreamer-1.0:/usr/lib/aarch64-linux-gnu/gstreamer-1.0:$APPDIR/usr/lib/aarch64-linux-gnu/gstreamer-1.0
|
||||||
GST_PLUGIN_SYSTEM_PATH: /lib64/gstreamer-1.0:/usr/lib/aarch64-linux-gnu/gstreamer-1.0:$APPDIR/usr/lib/aarch64-linux-gnu/gstreamer-1.0
|
GST_PLUGIN_SYSTEM_PATH: /lib64/gstreamer-1.0:/usr/lib/aarch64-linux-gnu/gstreamer-1.0:$APPDIR/usr/lib/aarch64-linux-gnu/gstreamer-1.0
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ AppDir:
|
|||||||
id: rustdesk
|
id: rustdesk
|
||||||
name: rustdesk
|
name: rustdesk
|
||||||
icon: rustdesk
|
icon: rustdesk
|
||||||
version: 1.4.9
|
version: 1.5.0
|
||||||
exec: usr/share/rustdesk/rustdesk
|
exec: usr/share/rustdesk/rustdesk
|
||||||
exec_args: $@
|
exec_args: $@
|
||||||
apt:
|
apt:
|
||||||
@@ -61,7 +61,6 @@ AppDir:
|
|||||||
- libpulse0
|
- libpulse0
|
||||||
- packagekit-gtk3-module
|
- packagekit-gtk3-module
|
||||||
- libcanberra-gtk3-module
|
- libcanberra-gtk3-module
|
||||||
- libpam0g
|
|
||||||
- libdrm2
|
- libdrm2
|
||||||
exclude:
|
exclude:
|
||||||
- humanity-icon-theme
|
- humanity-icon-theme
|
||||||
@@ -80,6 +79,13 @@ AppDir:
|
|||||||
env:
|
env:
|
||||||
GIO_MODULE_DIR: /lib64/gio/modules:/usr/lib/x86_64-linux-gnu/gio/modules:$APPDIR/usr/lib/x86_64-linux-gnu/gio/modules
|
GIO_MODULE_DIR: /lib64/gio/modules:/usr/lib/x86_64-linux-gnu/gio/modules:$APPDIR/usr/lib/x86_64-linux-gnu/gio/modules
|
||||||
GDK_BACKEND: x11
|
GDK_BACKEND: x11
|
||||||
|
# AppRun sets these to "$APPDIR/...:$XDG_DATA_DIRS", and setting them at all suppresses the XDG
|
||||||
|
# defaults, so a host that leaves them unset loses /usr/share and /etc/xdg. gdk-pixbuf 2.43+
|
||||||
|
# (Arch, Fedora) then finds no glycin loaders and every PNG decode fails, aborting on the first
|
||||||
|
# remote cursor. The host value goes last: unset it expands to an empty element, which GLib
|
||||||
|
# resolves against the CWD, and that must not outrank the defaults below.
|
||||||
|
XDG_DATA_DIRS: $APPDIR/usr/local/share:$APPDIR/usr/share:/usr/local/share:/usr/share:$XDG_DATA_DIRS
|
||||||
|
XDG_CONFIG_DIRS: $APPDIR/etc/xdg:/etc/xdg:$XDG_CONFIG_DIRS
|
||||||
APPDIR_LIBRARY_PATH: /lib64:/usr/lib/x86_64-linux-gnu:$APPDIR/lib/x86_64-linux-gnu:$APPDIR/lib/x86_64-linux-gnu/security:$APPDIR/lib/systemd:$APPDIR/usr/lib/x86_64-linux-gnu:$APPDIR/usr/lib/x86_64-linux-gnu/gdk-pixbuf-2.0/2.10.0/loaders:$APPDIR/usr/lib/x86_64-linux-gnu/gstreamer-1.0:$APPDIR/usr/lib/x86_64-linux-gnu/gtk-3.0/3.0.0/immodules:$APPDIR/usr/lib/x86_64-linux-gnu/gtk-3.0/3.0.0/printbackends:$APPDIR/usr/lib/x86_64-linux-gnu/krb5/plugins/preauth:$APPDIR/usr/lib/x86_64-linux-gnu/libcanberra-0.30:$APPDIR/usr/lib/x86_64-linux-gnu/pulseaudio:$APPDIR/usr/lib/x86_64-linux-gnu/sasl2:$APPDIR/usr/lib/x86_64-linux-gnu/vdpau:$APPDIR/usr/share/rustdesk/lib:$APPDIR/lib/x86_64
|
APPDIR_LIBRARY_PATH: /lib64:/usr/lib/x86_64-linux-gnu:$APPDIR/lib/x86_64-linux-gnu:$APPDIR/lib/x86_64-linux-gnu/security:$APPDIR/lib/systemd:$APPDIR/usr/lib/x86_64-linux-gnu:$APPDIR/usr/lib/x86_64-linux-gnu/gdk-pixbuf-2.0/2.10.0/loaders:$APPDIR/usr/lib/x86_64-linux-gnu/gstreamer-1.0:$APPDIR/usr/lib/x86_64-linux-gnu/gtk-3.0/3.0.0/immodules:$APPDIR/usr/lib/x86_64-linux-gnu/gtk-3.0/3.0.0/printbackends:$APPDIR/usr/lib/x86_64-linux-gnu/krb5/plugins/preauth:$APPDIR/usr/lib/x86_64-linux-gnu/libcanberra-0.30:$APPDIR/usr/lib/x86_64-linux-gnu/pulseaudio:$APPDIR/usr/lib/x86_64-linux-gnu/sasl2:$APPDIR/usr/lib/x86_64-linux-gnu/vdpau:$APPDIR/usr/share/rustdesk/lib:$APPDIR/lib/x86_64
|
||||||
GST_PLUGIN_PATH: /lib64/gstreamer-1.0:/usr/lib/x86_64-linux-gnu/gstreamer-1.0:$APPDIR/usr/lib/x86_64-linux-gnu/gstreamer-1.0
|
GST_PLUGIN_PATH: /lib64/gstreamer-1.0:/usr/lib/x86_64-linux-gnu/gstreamer-1.0:$APPDIR/usr/lib/x86_64-linux-gnu/gstreamer-1.0
|
||||||
GST_PLUGIN_SYSTEM_PATH: /lib64/gstreamer-1.0:/usr/lib/x86_64-linux-gnu/gstreamer-1.0:$APPDIR/usr/lib/x86_64-linux-gnu/gstreamer-1.0
|
GST_PLUGIN_SYSTEM_PATH: /lib64/gstreamer-1.0:/usr/lib/x86_64-linux-gnu/gstreamer-1.0:$APPDIR/usr/lib/x86_64-linux-gnu/gstreamer-1.0
|
||||||
|
|||||||
20
build.py
20
build.py
@@ -364,7 +364,7 @@ Version: %s
|
|||||||
Architecture: %s
|
Architecture: %s
|
||||||
Maintainer: rustdesk <info@rustdesk.com>
|
Maintainer: rustdesk <info@rustdesk.com>
|
||||||
Homepage: https://rustdesk.com
|
Homepage: https://rustdesk.com
|
||||||
Depends: libgtk-3-0t64 | libgtk-3-0, libxcb-randr0, libxdo3 | libxdo4, libxfixes3, libxcb-shape0, libxcb-xfixes0, libasound2t64 | libasound2, libsystemd0, curl, libva2, libva-drm2, libva-x11-2, libgstreamer-plugins-base1.0-0, libpam0g, gstreamer1.0-pipewire%s
|
Depends: libgtk-3-0t64 | libgtk-3-0, libxcb-randr0, libxdo3 | libxdo4, libxfixes3, libxcb-shape0, libxcb-xfixes0, libasound2t64 | libasound2, libsystemd0, curl, libva2, libva-drm2, libva-x11-2, libgstreamer-plugins-base1.0-0, gstreamer1.0-pipewire%s
|
||||||
Recommends: libayatana-appindicator3-1
|
Recommends: libayatana-appindicator3-1
|
||||||
Description: A remote control software.
|
Description: A remote control software.
|
||||||
|
|
||||||
@@ -390,9 +390,9 @@ def ffi_bindgen_function_refactor():
|
|||||||
# The commit is fetched directly by sha, so no branch or tag name takes part in the build: see
|
# The commit is fetched directly by sha, so no branch or tag name takes part in the build: see
|
||||||
# build_libdrmtap_so(). This is the SINGLE source of truth for the pin, deliberately not duplicated in
|
# build_libdrmtap_so(). This is the SINGLE source of truth for the pin, deliberately not duplicated in
|
||||||
# any workflow, so a bump is one edit here (plus the informational version comment in
|
# any workflow, so a bump is one edit here (plus the informational version comment in
|
||||||
# libs/scrap/Cargo.toml). This commit is libdrmtap v0.5.2.
|
# libs/scrap/Cargo.toml). This commit is libdrmtap v0.5.4.
|
||||||
LIBDRMTAP_REPO_PINNED = 'https://github.com/rustdesk-org/libdrmtap'
|
LIBDRMTAP_REPO_PINNED = 'https://github.com/rustdesk-org/libdrmtap'
|
||||||
LIBDRMTAP_SHA_PINNED = '653de8c774bc245eaf960611ca7a136f7a544d21'
|
LIBDRMTAP_SHA_PINNED = '5da68a3a368db569716d0d0f11cefacbb11b2290'
|
||||||
LIBDRMTAP_REPO = os.environ.get('DRMTAP_REPO', LIBDRMTAP_REPO_PINNED)
|
LIBDRMTAP_REPO = os.environ.get('DRMTAP_REPO', LIBDRMTAP_REPO_PINNED)
|
||||||
LIBDRMTAP_SHA = os.environ.get('DRMTAP_SHA', LIBDRMTAP_SHA_PINNED)
|
LIBDRMTAP_SHA = os.environ.get('DRMTAP_SHA', LIBDRMTAP_SHA_PINNED)
|
||||||
# Every way of getting a different .so than the pin needs the same explicit opt-in. Otherwise the
|
# Every way of getting a different .so than the pin needs the same explicit opt-in. Otherwise the
|
||||||
@@ -704,8 +704,6 @@ def build_flutter_deb(version, features):
|
|||||||
system2('flutter build linux --release')
|
system2('flutter build linux --release')
|
||||||
system2('mkdir -p tmpdeb/usr/bin/')
|
system2('mkdir -p tmpdeb/usr/bin/')
|
||||||
system2('mkdir -p tmpdeb/usr/share/rustdesk')
|
system2('mkdir -p tmpdeb/usr/share/rustdesk')
|
||||||
system2('mkdir -p tmpdeb/etc/rustdesk/')
|
|
||||||
system2('mkdir -p tmpdeb/etc/pam.d/')
|
|
||||||
system2('mkdir -p tmpdeb/usr/share/rustdesk/files/systemd/')
|
system2('mkdir -p tmpdeb/usr/share/rustdesk/files/systemd/')
|
||||||
system2('mkdir -p tmpdeb/usr/share/icons/hicolor/256x256/apps/')
|
system2('mkdir -p tmpdeb/usr/share/icons/hicolor/256x256/apps/')
|
||||||
system2('mkdir -p tmpdeb/usr/share/icons/hicolor/scalable/apps/')
|
system2('mkdir -p tmpdeb/usr/share/icons/hicolor/scalable/apps/')
|
||||||
@@ -724,12 +722,6 @@ def build_flutter_deb(version, features):
|
|||||||
'cp ../res/rustdesk.desktop tmpdeb/usr/share/applications/rustdesk.desktop')
|
'cp ../res/rustdesk.desktop tmpdeb/usr/share/applications/rustdesk.desktop')
|
||||||
system2(
|
system2(
|
||||||
'cp ../res/rustdesk-link.desktop tmpdeb/usr/share/applications/rustdesk-link.desktop')
|
'cp ../res/rustdesk-link.desktop tmpdeb/usr/share/applications/rustdesk-link.desktop')
|
||||||
system2(
|
|
||||||
'cp ../res/startwm.sh tmpdeb/etc/rustdesk/')
|
|
||||||
system2(
|
|
||||||
'cp ../res/xorg.conf tmpdeb/etc/rustdesk/')
|
|
||||||
system2(
|
|
||||||
'cp ../res/pam.d/rustdesk.debian tmpdeb/etc/pam.d/rustdesk')
|
|
||||||
system2(
|
system2(
|
||||||
"echo \"#!/bin/sh\" >> tmpdeb/usr/share/rustdesk/files/polkit && chmod a+x tmpdeb/usr/share/rustdesk/files/polkit")
|
"echo \"#!/bin/sh\" >> tmpdeb/usr/share/rustdesk/files/polkit && chmod a+x tmpdeb/usr/share/rustdesk/files/polkit")
|
||||||
# Bundle libdrmtap.so only when this build actually enabled the `drm` feature, so stock packages
|
# Bundle libdrmtap.so only when this build actually enabled the `drm` feature, so stock packages
|
||||||
@@ -1132,13 +1124,7 @@ def main():
|
|||||||
'cp res/rustdesk.desktop tmpdeb/usr/share/applications/rustdesk.desktop')
|
'cp res/rustdesk.desktop tmpdeb/usr/share/applications/rustdesk.desktop')
|
||||||
system2(
|
system2(
|
||||||
'cp res/rustdesk-link.desktop tmpdeb/usr/share/applications/rustdesk-link.desktop')
|
'cp res/rustdesk-link.desktop tmpdeb/usr/share/applications/rustdesk-link.desktop')
|
||||||
os.system('mkdir -p tmpdeb/etc/rustdesk/')
|
|
||||||
os.system('cp -a res/startwm.sh tmpdeb/etc/rustdesk/')
|
|
||||||
os.system('mkdir -p tmpdeb/etc/X11/rustdesk/')
|
|
||||||
os.system('cp res/xorg.conf tmpdeb/etc/X11/rustdesk/')
|
|
||||||
os.system('cp -a DEBIAN/* tmpdeb/DEBIAN/')
|
os.system('cp -a DEBIAN/* tmpdeb/DEBIAN/')
|
||||||
os.system('mkdir -p tmpdeb/etc/pam.d/')
|
|
||||||
os.system('cp pam.d/rustdesk.debian tmpdeb/etc/pam.d/rustdesk')
|
|
||||||
system2('strip tmpdeb/usr/bin/rustdesk')
|
system2('strip tmpdeb/usr/bin/rustdesk')
|
||||||
system2('mkdir -p tmpdeb/usr/share/rustdesk')
|
system2('mkdir -p tmpdeb/usr/share/rustdesk')
|
||||||
system2('mv tmpdeb/usr/bin/rustdesk tmpdeb/usr/share/rustdesk/')
|
system2('mv tmpdeb/usr/bin/rustdesk tmpdeb/usr/share/rustdesk/')
|
||||||
|
|||||||
13
build.rs
13
build.rs
@@ -43,6 +43,15 @@ fn build_manifest() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// bionic only exports getifaddrs()/freeifaddrs() from API 24, while the jniLibs
|
||||||
|
// are built against the API 21 sysroot (flutter/ndk_*.sh). webrtc-util calls
|
||||||
|
// them, so without this the android link fails on undefined symbols.
|
||||||
|
fn build_android_ifaddrs() {
|
||||||
|
let file = "src/platform/android_ifaddrs.c";
|
||||||
|
cc::Build::new().file(file).compile("android_ifaddrs");
|
||||||
|
println!("cargo:rerun-if-changed={}", file);
|
||||||
|
}
|
||||||
|
|
||||||
fn install_android_deps() {
|
fn install_android_deps() {
|
||||||
let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap();
|
let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap();
|
||||||
if target_os != "android" {
|
if target_os != "android" {
|
||||||
@@ -72,7 +81,6 @@ fn install_android_deps() {
|
|||||||
path.join("lib").to_str().unwrap()
|
path.join("lib").to_str().unwrap()
|
||||||
);
|
);
|
||||||
println!("cargo:rustc-link-lib=ndk_compat");
|
println!("cargo:rustc-link-lib=ndk_compat");
|
||||||
println!("cargo:rustc-link-lib=oboe");
|
|
||||||
println!("cargo:rustc-link-lib=c++");
|
println!("cargo:rustc-link-lib=c++");
|
||||||
println!("cargo:rustc-link-lib=OpenSLES");
|
println!("cargo:rustc-link-lib=OpenSLES");
|
||||||
}
|
}
|
||||||
@@ -90,5 +98,8 @@ fn main() {
|
|||||||
build_mac();
|
build_mac();
|
||||||
println!("cargo:rustc-link-lib=framework=ApplicationServices");
|
println!("cargo:rustc-link-lib=framework=ApplicationServices");
|
||||||
}
|
}
|
||||||
|
if target_os == "android" {
|
||||||
|
build_android_ifaddrs();
|
||||||
|
}
|
||||||
println!("cargo:rerun-if-changed=build.rs");
|
println!("cargo:rerun-if-changed=build.rs");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ Untuk instruksi Git yang lebih lanjut, cek disini [GitHub workflow 101](https://
|
|||||||
|
|
||||||
## Tindakan
|
## Tindakan
|
||||||
|
|
||||||
<https://github.com/rustdesk/rustdesk/blob/master/docs/CODE_OF_CONDUCT-ID.md>
|
<https://github.com/rustdesk/rustdesk/blob/master/docs/CODE_OF_CONDUCT.md>
|
||||||
|
|
||||||
## Komunikasi
|
## Komunikasi
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ Per istruzioni specifiche su git, vedi [Workflow GitHub - 101](https://github.co
|
|||||||
|
|
||||||
## Condotta
|
## Condotta
|
||||||
|
|
||||||
https://github.com/rustdesk/rustdesk/blob/master/docs/CODE_OF_CONDUCT-IT.md
|
https://github.com/rustdesk/rustdesk/blob/master/docs/CODE_OF_CONDUCT.md
|
||||||
|
|
||||||
## Comunicazioni
|
## Comunicazioni
|
||||||
|
|
||||||
|
|||||||
@@ -160,7 +160,6 @@ RustDesk يرجى التأكد من أنك تنفذ هذه الأوامر من
|
|||||||
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: أو المنقول عن بُعد (TCP hole punching) انتظر الاتصال المباشر [rustdesk-server](https://github.com/rustdesk/rustdesk-server) الإتصال ب
|
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: أو المنقول عن بُعد (TCP hole punching) انتظر الاتصال المباشر [rustdesk-server](https://github.com/rustdesk/rustdesk-server) الإتصال ب
|
||||||
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: رمز خاص بكل منصة
|
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: رمز خاص بكل منصة
|
||||||
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: رمز الهاتف المحمول
|
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: رمز الهاتف المحمول
|
||||||
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**:Flutter لعميل الويب الخاص ب Javascript
|
|
||||||
|
|
||||||
## لقطات
|
## لقطات
|
||||||
|
|
||||||
|
|||||||
@@ -144,7 +144,6 @@ Ujistěte se, že tyto příkazy spouštíte z kořenového adresáře RustDesk,
|
|||||||
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: komunikace s [rustdesk-server](https://github.com/rustdesk/rustdesk-server), očekávání vzdálených příméhých („proděrováváním“ TCP) nebo předávaných (relay) spojení
|
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: komunikace s [rustdesk-server](https://github.com/rustdesk/rustdesk-server), očekávání vzdálených příméhých („proděrováváním“ TCP) nebo předávaných (relay) spojení
|
||||||
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: zdrojové kódy, specifické pro jednotlivé platformy
|
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: zdrojové kódy, specifické pro jednotlivé platformy
|
||||||
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: zdrojové kódy pro použití s aplikačním rámcem (framework) Flutter pro mobilní platformy
|
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: zdrojové kódy pro použití s aplikačním rámcem (framework) Flutter pro mobilní platformy
|
||||||
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: Javascript pro Flutter webový klient
|
|
||||||
|
|
||||||
## Ukázky
|
## Ukázky
|
||||||
|
|
||||||
|
|||||||
@@ -66,19 +66,19 @@ Bitte laden Sie die dynamische Bibliothek Sciter selbst herunter.
|
|||||||
```sh
|
```sh
|
||||||
sudo apt install -y zip g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev \
|
sudo apt install -y zip g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev \
|
||||||
libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake make \
|
libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake make \
|
||||||
libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libpam0g-dev
|
libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev
|
||||||
```
|
```
|
||||||
|
|
||||||
### openSUSE Tumbleweed
|
### openSUSE Tumbleweed
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel pam-devel
|
sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel
|
||||||
```
|
```
|
||||||
|
|
||||||
### Fedora 28 (CentOS 8)
|
### Fedora 28 (CentOS 8)
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel gstreamer1-devel gstreamer1-plugins-base-devel pam-devel
|
sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel gstreamer1-devel gstreamer1-plugins-base-devel
|
||||||
```
|
```
|
||||||
|
|
||||||
### Arch (Manjaro)
|
### Arch (Manjaro)
|
||||||
@@ -168,7 +168,6 @@ Bitte stellen Sie sicher, dass Sie diese Befehle im Stammverzeichnis des RustDes
|
|||||||
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Mit [rustdesk-server](https://github.com/rustdesk/rustdesk-server) kommunizieren, warten auf direkte (TCP hole punching) oder weitergeleitete Verbindung
|
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Mit [rustdesk-server](https://github.com/rustdesk/rustdesk-server) kommunizieren, warten auf direkte (TCP hole punching) oder weitergeleitete Verbindung
|
||||||
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: Plattformspezifischer Code
|
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: Plattformspezifischer Code
|
||||||
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: Flutter-Code für Handys
|
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: Flutter-Code für Handys
|
||||||
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: JavaScript für Flutter-Webclient
|
|
||||||
|
|
||||||
## Screenshots
|
## Screenshots
|
||||||
|
|
||||||
|
|||||||
@@ -62,19 +62,19 @@ Por favor descarga la librería dinámica de Sciter tú mismo.
|
|||||||
```sh
|
```sh
|
||||||
sudo apt install -y zip g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev \
|
sudo apt install -y zip g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev \
|
||||||
libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake make \
|
libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake make \
|
||||||
libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libpam0g-dev
|
libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev
|
||||||
```
|
```
|
||||||
|
|
||||||
### openSUSE Tumbleweed
|
### openSUSE Tumbleweed
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel pam-devel
|
sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel
|
||||||
```
|
```
|
||||||
|
|
||||||
### Fedora 28 (CentOS 8)
|
### Fedora 28 (CentOS 8)
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel gstreamer1-devel gstreamer1-plugins-base-devel pam-devel
|
sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel gstreamer1-devel gstreamer1-plugins-base-devel
|
||||||
```
|
```
|
||||||
|
|
||||||
### Arch (Manjaro)
|
### Arch (Manjaro)
|
||||||
@@ -163,7 +163,6 @@ Por favor, asegurate de que estás ejecutando estos comandos desde la raíz del
|
|||||||
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Comunicación con [rustdesk-server](https://github.com/rustdesk/rustdesk-server), esperar la conexión remota directa ("TCP hole punching") o conexión indirecta ("relayed")
|
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Comunicación con [rustdesk-server](https://github.com/rustdesk/rustdesk-server), esperar la conexión remota directa ("TCP hole punching") o conexión indirecta ("relayed")
|
||||||
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: código específico de cada plataforma
|
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: código específico de cada plataforma
|
||||||
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: Flutter, código para moviles
|
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: Flutter, código para moviles
|
||||||
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: Javascript para el cliente web Flutter
|
|
||||||
|
|
||||||
> [!Precaución]
|
> [!Precaución]
|
||||||
> **Descargo de responsabilidad por uso indebido:** <br>
|
> **Descargo de responsabilidad por uso indebido:** <br>
|
||||||
|
|||||||
@@ -146,7 +146,6 @@ target/release/rustdesk
|
|||||||
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Communicate with [rustdesk-server](https://github.com/rustdesk/rustdesk-server), wait for remote direct (TCP hole punching) or relayed connection
|
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Communicate with [rustdesk-server](https://github.com/rustdesk/rustdesk-server), wait for remote direct (TCP hole punching) or relayed connection
|
||||||
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: platform specific code
|
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: platform specific code
|
||||||
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: Flutter code for mobile
|
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: Flutter code for mobile
|
||||||
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: Javascript for Flutter web client
|
|
||||||
|
|
||||||
## تصاویر محیط نرمافزار
|
## تصاویر محیط نرمافزار
|
||||||
|
|
||||||
|
|||||||
@@ -158,7 +158,6 @@ target/release/rustdesk
|
|||||||
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Communicate with [rustdesk-server](https://github.com/rustdesk/rustdesk-server), wait for remote direct (TCP hole punching) or relayed connection
|
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Communicate with [rustdesk-server](https://github.com/rustdesk/rustdesk-server), wait for remote direct (TCP hole punching) or relayed connection
|
||||||
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: platform specific code
|
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: platform specific code
|
||||||
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: Flutter code for mobile
|
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: Flutter code for mobile
|
||||||
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: JavaScript for Flutter web client
|
|
||||||
|
|
||||||
## Στιγμιότυπα
|
## Στιγμιότυπα
|
||||||
|
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ A telefonos verziók Flutter-t hasznának. Később lehetséges hogy Sciterről
|
|||||||
|
|
||||||
- Futtasd a `cargo run` parancsot
|
- Futtasd a `cargo run` parancsot
|
||||||
|
|
||||||
## [Építés](https://rustdesk.com/docs/hu/dev/build/)
|
## [Építés](https://rustdesk.com/docs/en/dev/build/)
|
||||||
|
|
||||||
## Hogyan építs Linuxon
|
## Hogyan építs Linuxon
|
||||||
|
|
||||||
@@ -150,7 +150,6 @@ Kérlek mindenképpen nézd meg hogy ezeket a parancsokat a root RustDesk mappá
|
|||||||
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Communicate with [rustdesk-server](https://github.com/rustdesk/rustdesk-server), wait for remote direct (TCP hole punching) or relayed connection
|
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Communicate with [rustdesk-server](https://github.com/rustdesk/rustdesk-server), wait for remote direct (TCP hole punching) or relayed connection
|
||||||
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: platform specific code
|
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: platform specific code
|
||||||
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: Flutter code for mobile
|
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: Flutter code for mobile
|
||||||
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: Javascript for Flutter web client
|
|
||||||
|
|
||||||
## Képernyőképek
|
## Képernyőképek
|
||||||
|
|
||||||
|
|||||||
@@ -162,7 +162,6 @@ Assicurati di eseguire questi comandi dalla radice del repository RustDesk, altr
|
|||||||
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: comunica con [rustdesk-server](https://github.com/rustdesk/rustdesk-server), attende la connessione remota diretta (TCP hole punching) oppure indiretta (relayed)
|
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: comunica con [rustdesk-server](https://github.com/rustdesk/rustdesk-server), attende la connessione remota diretta (TCP hole punching) oppure indiretta (relayed)
|
||||||
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: codice specifico della piattaforma
|
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: codice specifico della piattaforma
|
||||||
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: codice Flutter per desktop e mobile
|
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: codice Flutter per desktop e mobile
|
||||||
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: JavaScript per client web Flutter
|
|
||||||
|
|
||||||
> [!Attenzione]
|
> [!Attenzione]
|
||||||
> **Dichiarazione di non responsabilità per uso improprio:** <br>
|
> **Dichiarazione di non responsabilità per uso improprio:** <br>
|
||||||
|
|||||||
@@ -166,7 +166,6 @@ target/release/rustdesk
|
|||||||
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: [rustdesk-server](https://github.com/rustdesk/rustdesk-server)と通信し、リモートの直接接続(TCPホールパンチング)や中継接続を担う。
|
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: [rustdesk-server](https://github.com/rustdesk/rustdesk-server)と通信し、リモートの直接接続(TCPホールパンチング)や中継接続を担う。
|
||||||
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: プラットフォーム固有のコード
|
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: プラットフォーム固有のコード
|
||||||
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: デスクトップとモバイル向けのFlutterコード
|
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: デスクトップとモバイル向けのFlutterコード
|
||||||
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: Flutterウェブクライアント向けのJavaScript
|
|
||||||
|
|
||||||
> [!注意]
|
> [!注意]
|
||||||
> **:不正使用に関する免責事項** <br>
|
> **:不正使用に関する免責事項** <br>
|
||||||
|
|||||||
@@ -66,19 +66,19 @@ Sciter 동적 라이브러리를 직접 다운로드하세요.
|
|||||||
```sh
|
```sh
|
||||||
sudo apt install -y zip g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev \
|
sudo apt install -y zip g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev \
|
||||||
libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake make \
|
libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake make \
|
||||||
libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libpam0g-dev
|
libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev
|
||||||
```
|
```
|
||||||
|
|
||||||
### openSUSE Tumbleweed
|
### openSUSE Tumbleweed
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel pam-devel
|
sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel
|
||||||
```
|
```
|
||||||
|
|
||||||
### Fedora 28 (CentOS 8)
|
### Fedora 28 (CentOS 8)
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel gstreamer1-devel gstreamer1-plugins-base-devel pam-devel
|
sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel gstreamer1-devel gstreamer1-plugins-base-devel
|
||||||
```
|
```
|
||||||
|
|
||||||
### Arch (Manjaro)
|
### Arch (Manjaro)
|
||||||
@@ -168,7 +168,6 @@ RustDesk 리포지토리의 루트에서 이러한 명령을 실행하고 있는
|
|||||||
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: [rustdesk-server](https://github.com/rustdesk/rustdesk-server)와 통신, 원격 다이렉트 (TCP 홀 펀칭) 또는 릴레이 연결 대기
|
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: [rustdesk-server](https://github.com/rustdesk/rustdesk-server)와 통신, 원격 다이렉트 (TCP 홀 펀칭) 또는 릴레이 연결 대기
|
||||||
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: 플랫폼별 코드
|
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: 플랫폼별 코드
|
||||||
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: 데스크톱 및 모바일용 Flutter 코드
|
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: 데스크톱 및 모바일용 Flutter 코드
|
||||||
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/v1/js)**: Flutter 웹 클라이언트용 JavaScript
|
|
||||||
|
|
||||||
## 스크린샷
|
## 스크린샷
|
||||||
|
|
||||||
|
|||||||
@@ -62,19 +62,19 @@ Venligst last ned Sciters dynamiske bibliotek selv.
|
|||||||
```sh
|
```sh
|
||||||
sudo apt install -y zip g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev \
|
sudo apt install -y zip g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev \
|
||||||
libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake make \
|
libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake make \
|
||||||
libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libpam0g-dev
|
libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev
|
||||||
```
|
```
|
||||||
|
|
||||||
### openSUSE Tumbleweed
|
### openSUSE Tumbleweed
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel pam-devel
|
sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel
|
||||||
```
|
```
|
||||||
|
|
||||||
### Fedora 28 (CentOS 8)
|
### Fedora 28 (CentOS 8)
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel gstreamer1-devel gstreamer1-plugins-base-devel pam-devel
|
sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel gstreamer1-devel gstreamer1-plugins-base-devel
|
||||||
```
|
```
|
||||||
|
|
||||||
### Arch (Manjaro)
|
### Arch (Manjaro)
|
||||||
@@ -163,7 +163,6 @@ Venligst pass på att du kjører disse kommandoene fra roten av RustDesk reposit
|
|||||||
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Kommunikasjon med [rustdesk-server](https://github.com/rustdesk/rustdesk-server), vent på direkte fjernstyring (TCP hulling) eller vidresendt tilkobling
|
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Kommunikasjon med [rustdesk-server](https://github.com/rustdesk/rustdesk-server), vent på direkte fjernstyring (TCP hulling) eller vidresendt tilkobling
|
||||||
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: platform spesefik kode
|
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: platform spesefik kode
|
||||||
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: Flutter kode for desktop og mobil
|
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: Flutter kode for desktop og mobil
|
||||||
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: JavaScript for Flutter nettsted klient
|
|
||||||
|
|
||||||
## Skjermbilder
|
## Skjermbilder
|
||||||
|
|
||||||
|
|||||||
@@ -155,7 +155,6 @@ Upewnij się, że uruchamiasz te polecenia z katalogu głównego repozytorium Ru
|
|||||||
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Komunikacja z [rustdesk-server](https://github.com/rustdesk/rustdesk-server), czekanie na bezpośrednie (odpytywanie TCP) lub przekazywane połączenie
|
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Komunikacja z [rustdesk-server](https://github.com/rustdesk/rustdesk-server), czekanie na bezpośrednie (odpytywanie TCP) lub przekazywane połączenie
|
||||||
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: kod specyficzny dla danej platformy
|
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: kod specyficzny dla danej platformy
|
||||||
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: kod Flutter dla urządzeń mobilnych
|
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: kod Flutter dla urządzeń mobilnych
|
||||||
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: JavaScript dla Flutter - klient web
|
|
||||||
|
|
||||||
## Zrzuty ekranu
|
## Zrzuty ekranu
|
||||||
|
|
||||||
|
|||||||
@@ -64,19 +64,19 @@ Por favor, faça o download da biblioteca dinâmica do Sciter por conta própria
|
|||||||
### Ubuntu 18 (Debian 10)
|
### Ubuntu 18 (Debian 10)
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
sudo apt install -y zip g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake make libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libpam0g-dev
|
sudo apt install -y zip g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake make libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev
|
||||||
```
|
```
|
||||||
|
|
||||||
### openSUSE Tumbleweed
|
### openSUSE Tumbleweed
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel pam-devel
|
sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel
|
||||||
```
|
```
|
||||||
|
|
||||||
### Fedora 28 (CentOS 8)
|
### Fedora 28 (CentOS 8)
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel gstreamer1-devel gstreamer1-plugins-base-devel pam-devel
|
sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel gstreamer1-devel gstreamer1-plugins-base-devel
|
||||||
```
|
```
|
||||||
|
|
||||||
### Arch (Manjaro)
|
### Arch (Manjaro)
|
||||||
@@ -166,7 +166,6 @@ Certifique-se de executar esses comandos a partir da raiz do repositório do Rus
|
|||||||
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Comunica-se com o [rustdesk-server](https://github.com/rustdesk/rustdesk-server), aguarda por conexão remota direta (perfuração de túnel TCP / hole punching) ou retransmitida.
|
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Comunica-se com o [rustdesk-server](https://github.com/rustdesk/rustdesk-server), aguarda por conexão remota direta (perfuração de túnel TCP / hole punching) ou retransmitida.
|
||||||
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: código específico de cada plataforma.
|
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: código específico de cada plataforma.
|
||||||
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: código Flutter para desktop e dispositivos móveis.
|
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: código Flutter para desktop e dispositivos móveis.
|
||||||
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/v1/js)**: JavaScript para o cliente web do Flutter.
|
|
||||||
|
|
||||||
## Capturas de Tela
|
## Capturas de Tela
|
||||||
|
|
||||||
|
|||||||
@@ -66,19 +66,19 @@ Te rugăm să descarci singur librăria dinamică Sciter.
|
|||||||
```sh
|
```sh
|
||||||
sudo apt install -y zip g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev \
|
sudo apt install -y zip g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev \
|
||||||
libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake make \
|
libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake make \
|
||||||
libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libpam0g-dev
|
libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev
|
||||||
```
|
```
|
||||||
|
|
||||||
### openSUSE Tumbleweed
|
### openSUSE Tumbleweed
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel pam-devel
|
sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel
|
||||||
```
|
```
|
||||||
|
|
||||||
### Fedora 28 (CentOS 8)
|
### Fedora 28 (CentOS 8)
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel gstreamer1-devel gstreamer1-plugins-base-devel pam-devel
|
sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel gstreamer1-devel gstreamer1-plugins-base-devel
|
||||||
```
|
```
|
||||||
|
|
||||||
### Arch (Manjaro)
|
### Arch (Manjaro)
|
||||||
@@ -168,7 +168,6 @@ Asigură-te că rulezi aceste comenzi din rădăcina repository-ului RustDesk, a
|
|||||||
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: comunică cu [rustdesk-server](https://github.com/rustdesk/rustdesk-server), așteaptă conexiune directă remote (TCP hole punching) sau prin relay
|
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: comunică cu [rustdesk-server](https://github.com/rustdesk/rustdesk-server), așteaptă conexiune directă remote (TCP hole punching) sau prin relay
|
||||||
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: cod specific platformei
|
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: cod specific platformei
|
||||||
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: cod Flutter pentru desktop și mobil
|
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: cod Flutter pentru desktop și mobil
|
||||||
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/v1/js)**: JavaScript pentru clientul Flutter web
|
|
||||||
|
|
||||||
## Capturi de ecran
|
## Capturi de ecran
|
||||||
|
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ RustDesk приветствует вклад каждого. Ознакомьт
|
|||||||
|
|
||||||
- Выполните команду `cargo run`
|
- Выполните команду `cargo run`
|
||||||
|
|
||||||
## [Сборка](https://rustdesk.com/docs/ru/dev/build/)
|
## [Сборка](https://rustdesk.com/docs/en/dev/build/)
|
||||||
|
|
||||||
## Как собрать на Linux
|
## Как собрать на Linux
|
||||||
|
|
||||||
@@ -68,19 +68,19 @@ RustDesk приветствует вклад каждого. Ознакомьт
|
|||||||
```sh
|
```sh
|
||||||
sudo apt install -y zip g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev \
|
sudo apt install -y zip g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev \
|
||||||
libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake make \
|
libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake make \
|
||||||
libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libpam0g-dev
|
libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev
|
||||||
```
|
```
|
||||||
|
|
||||||
### openSUSE Tumbleweed
|
### openSUSE Tumbleweed
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel pam-devel
|
sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel
|
||||||
```
|
```
|
||||||
|
|
||||||
### Fedora 28 (CentOS 8)
|
### Fedora 28 (CentOS 8)
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel gstreamer1-devel gstreamer1-plugins-base-devel pam-devel
|
sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel gstreamer1-devel gstreamer1-plugins-base-devel
|
||||||
```
|
```
|
||||||
|
|
||||||
### Arch (Manjaro)
|
### Arch (Manjaro)
|
||||||
@@ -170,7 +170,6 @@ target/release/rustdesk
|
|||||||
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: связь с [сервером RustDesk](https://github.com/rustdesk/rustdesk-server), ожидает удаленного прямого (через TCP hole punching) или ретранслируемого соединения
|
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: связь с [сервером RustDesk](https://github.com/rustdesk/rustdesk-server), ожидает удаленного прямого (через TCP hole punching) или ретранслируемого соединения
|
||||||
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: специфичный для платформы код
|
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: специфичный для платформы код
|
||||||
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: код Flutter для ПК-версии и мобильных устройств
|
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: код Flutter для ПК-версии и мобильных устройств
|
||||||
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/v1/js)**: JavaScript для Web-клиента Flutter
|
|
||||||
|
|
||||||
## Скриншоты
|
## Скриншоты
|
||||||
|
|
||||||
@@ -180,4 +179,4 @@ target/release/rustdesk
|
|||||||
|
|
||||||

|

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

|

|
||||||
|
|||||||
@@ -166,7 +166,6 @@ Lütfen bu komutları RustDesk reposunun root klasöründe çalıştırdığın
|
|||||||
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: [rustdesk-server](https://github.com/rustdesk/rustdesk-server) ile iletişime gir, remote direct(TCP delik açma) yada relay bağlantısı için bekle
|
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: [rustdesk-server](https://github.com/rustdesk/rustdesk-server) ile iletişime gir, remote direct(TCP delik açma) yada relay bağlantısı için bekle
|
||||||
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: platforma özgü kod
|
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: platforma özgü kod
|
||||||
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: Masaüstü ve mobil için Flutter kodu
|
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: Masaüstü ve mobil için Flutter kodu
|
||||||
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/v1/js)**: Flutter web istemcisi için JavaScript
|
|
||||||
|
|
||||||
|
|
||||||
## Ekran Görüntüleri
|
## Ekran Görüntüleri
|
||||||
|
|||||||
@@ -59,19 +59,19 @@ RustDesk вітає внесок кожного. Ознайомтеся з [CONT
|
|||||||
```sh
|
```sh
|
||||||
sudo apt install -y zip g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev \
|
sudo apt install -y zip g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev \
|
||||||
libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake make \
|
libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake make \
|
||||||
libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libpam0g-dev
|
libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev
|
||||||
```
|
```
|
||||||
|
|
||||||
### openSUSE Tumbleweed
|
### openSUSE Tumbleweed
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel pam-devel
|
sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel
|
||||||
```
|
```
|
||||||
|
|
||||||
### Fedora 28 (CentOS 8)
|
### Fedora 28 (CentOS 8)
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel gstreamer1-devel gstreamer1-plugins-base-devel pam-devel
|
sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel gstreamer1-devel gstreamer1-plugins-base-devel
|
||||||
```
|
```
|
||||||
|
|
||||||
### Arch (Manjaro)
|
### Arch (Manjaro)
|
||||||
@@ -160,7 +160,6 @@ target/release/rustdesk
|
|||||||
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: комунікація з [rustdesk-server](https://github.com/rustdesk/rustdesk-server), очікування віддаленого прямого (обхід TCP NAT) або ретрансльованого зʼєднання
|
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: комунікація з [rustdesk-server](https://github.com/rustdesk/rustdesk-server), очікування віддаленого прямого (обхід TCP NAT) або ретрансльованого зʼєднання
|
||||||
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: специфічний для платформи код
|
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: специфічний для платформи код
|
||||||
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: код Flutter для мобільних пристроїв
|
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: код Flutter для мобільних пристроїв
|
||||||
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: JavaScript для веб клієнта на Flutter
|
|
||||||
|
|
||||||
## Знімки екрана
|
## Знімки екрана
|
||||||
|
|
||||||
|
|||||||
@@ -148,7 +148,6 @@ Hãy đảm bảo rằng bạn đang chạy các lệnh này từ gốc của th
|
|||||||
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: giao tiếp với [rustdesk-server](https://github.com/rustdesk/rustdesk-server), đợi kết nối trực tiếp (TCP hole punching) hoặc kết nối được chuyển tiếp.
|
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: giao tiếp với [rustdesk-server](https://github.com/rustdesk/rustdesk-server), đợi kết nối trực tiếp (TCP hole punching) hoặc kết nối được chuyển tiếp.
|
||||||
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: mã nguồn riêng cho mỗi nền tảng
|
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: mã nguồn riêng cho mỗi nền tảng
|
||||||
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: Mã Flutter dành máy tính và điện thoại
|
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: Mã Flutter dành máy tính và điện thoại
|
||||||
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: Mã JavaScript dành cho giao diện trên web bằng Flutter
|
|
||||||
|
|
||||||
## Snapshot
|
## Snapshot
|
||||||
|
|
||||||
|
|||||||
@@ -220,7 +220,6 @@ target/release/rustdesk
|
|||||||
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: 与[rustdesk-server](https://github.com/rustdesk/rustdesk-server)保持UDP通讯, 等待远程连接(通过打洞直连或者中继)
|
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: 与[rustdesk-server](https://github.com/rustdesk/rustdesk-server)保持UDP通讯, 等待远程连接(通过打洞直连或者中继)
|
||||||
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: 平台服务相关代码
|
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: 平台服务相关代码
|
||||||
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: 适用于桌面和移动设备的 Flutter 代码
|
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: 适用于桌面和移动设备的 Flutter 代码
|
||||||
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: Flutter Web版本中的Javascript代码
|
|
||||||
|
|
||||||
## 截图
|
## 截图
|
||||||
|
|
||||||
|
|||||||
@@ -21,18 +21,6 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "pam",
|
|
||||||
"buildsystem": "autotools",
|
|
||||||
"config-opts": ["--disable-selinux"],
|
|
||||||
"sources": [
|
|
||||||
{
|
|
||||||
"type": "archive",
|
|
||||||
"url": "https://github.com/linux-pam/linux-pam/releases/download/v1.3.1/Linux-PAM-1.3.1.tar.xz",
|
|
||||||
"sha256": "eff47a4ecd833fbf18de9686632a70ee8d0794b79aecb217ebd0ce11db4cd0db"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "rustdesk",
|
"name": "rustdesk",
|
||||||
"buildsystem": "simple",
|
"buildsystem": "simple",
|
||||||
@@ -63,4 +51,4 @@
|
|||||||
"--socket=pulseaudio",
|
"--socket=pulseaudio",
|
||||||
"--talk-name=org.freedesktop.Flatpak"
|
"--talk-name=org.freedesktop.Flatpak"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,7 +82,8 @@ protobuf {
|
|||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 34
|
namespace "com.carriez.flutter_hbb"
|
||||||
|
compileSdkVersion 36
|
||||||
sourceSets {
|
sourceSets {
|
||||||
main.java.srcDirs += 'src/main/kotlin'
|
main.java.srcDirs += 'src/main/kotlin'
|
||||||
|
|
||||||
@@ -91,6 +92,7 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
|
coreLibraryDesugaringEnabled true
|
||||||
targetCompatibility JavaVersion.VERSION_1_8
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
}
|
}
|
||||||
@@ -99,7 +101,7 @@ android {
|
|||||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||||
applicationId "com.carriez.flutter_hbb"
|
applicationId "com.carriez.flutter_hbb"
|
||||||
minSdkVersion 22
|
minSdkVersion 22
|
||||||
targetSdkVersion 33
|
targetSdkVersion 36
|
||||||
versionCode flutterVersionCode.toInteger()
|
versionCode flutterVersionCode.toInteger()
|
||||||
versionName flutterVersionName
|
versionName flutterVersionName
|
||||||
}
|
}
|
||||||
@@ -128,6 +130,7 @@ flutter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.4'
|
||||||
implementation 'com.google.protobuf:protobuf-javalite:3.20.1'
|
implementation 'com.google.protobuf:protobuf-javalite:3.20.1'
|
||||||
implementation "androidx.media:media:1.6.0"
|
implementation "androidx.media:media:1.6.0"
|
||||||
implementation 'com.github.getActivity:XXPermissions:18.5'
|
implementation 'com.github.getActivity:XXPermissions:18.5'
|
||||||
|
|||||||
@@ -1,15 +1,19 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
package="com.carriez.flutter_hbb">
|
package="com.carriez.flutter_hbb">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
|
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" tools:node="remove" />
|
||||||
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" tools:node="remove" />
|
||||||
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" tools:node="remove" />
|
||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION" />
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
|
||||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||||
@@ -26,7 +30,6 @@
|
|||||||
android:name=".MainApplication"
|
android:name=".MainApplication"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="RustDesk"
|
android:label="RustDesk"
|
||||||
android:requestLegacyExternalStorage="true"
|
|
||||||
android:roundIcon="@mipmap/ic_launcher"
|
android:roundIcon="@mipmap/ic_launcher"
|
||||||
android:supportsRtl="true">
|
android:supportsRtl="true">
|
||||||
|
|
||||||
@@ -88,7 +91,12 @@
|
|||||||
<service
|
<service
|
||||||
android:name=".MainService"
|
android:name=".MainService"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:foregroundServiceType="mediaProjection" />
|
android:exported="false"
|
||||||
|
android:foregroundServiceType="specialUse|mediaProjection|microphone">
|
||||||
|
<property
|
||||||
|
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
|
||||||
|
android:value="@string/foreground_service_special_use_subtype" />
|
||||||
|
</service>
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".FloatingWindowService"
|
android:name=".FloatingWindowService"
|
||||||
|
|||||||
@@ -18,7 +18,33 @@ const val AUDIO_SAMPLE_RATE = 48000
|
|||||||
const val AUDIO_CHANNEL_MASK = AudioFormat.CHANNEL_IN_STEREO
|
const val AUDIO_CHANNEL_MASK = AudioFormat.CHANNEL_IN_STEREO
|
||||||
|
|
||||||
class AudioRecordHandle(private var context: Context, private var isVideoStart: ()->Boolean, private var isAudioStart: ()->Boolean) {
|
class AudioRecordHandle(private var context: Context, private var isVideoStart: ()->Boolean, private var isAudioStart: ()->Boolean) {
|
||||||
private val logTag = "LOG_AUDIO_RECORD_HANDLE"
|
companion object {
|
||||||
|
private const val LOG_TAG = "LOG_AUDIO_RECORD_HANDLE"
|
||||||
|
private const val NO_ACTIVE_PUBLISHERS = 0
|
||||||
|
private var activeAudioFramePublishers = NO_ACTIVE_PUBLISHERS
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
private fun acquireAudioFramePublisher() {
|
||||||
|
if (activeAudioFramePublishers == NO_ACTIVE_PUBLISHERS) {
|
||||||
|
FFI.setFrameRawEnable("audio", true)
|
||||||
|
}
|
||||||
|
activeAudioFramePublishers++
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
private fun releaseAudioFramePublisher() {
|
||||||
|
if (activeAudioFramePublishers == NO_ACTIVE_PUBLISHERS) {
|
||||||
|
Log.e(LOG_TAG, "No active audio frame publisher to release")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
activeAudioFramePublishers--
|
||||||
|
if (activeAudioFramePublishers == NO_ACTIVE_PUBLISHERS) {
|
||||||
|
FFI.setFrameRawEnable("audio", false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val logTag = LOG_TAG
|
||||||
|
|
||||||
private var audioRecorder: AudioRecord? = null
|
private var audioRecorder: AudioRecord? = null
|
||||||
private var audioReader: AudioReader? = null
|
private var audioReader: AudioReader? = null
|
||||||
@@ -79,48 +105,94 @@ class AudioRecordHandle(private var context: Context, private var isVideoStart:
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
// read f32 to byte , length * 4
|
// read f32 to byte , length * 4
|
||||||
minBufferSize = 2 * 4 * AudioRecord.getMinBufferSize(
|
val bufferSize = 2 * 4 * AudioRecord.getMinBufferSize(
|
||||||
AUDIO_SAMPLE_RATE,
|
AUDIO_SAMPLE_RATE,
|
||||||
AUDIO_CHANNEL_MASK,
|
AUDIO_CHANNEL_MASK,
|
||||||
AUDIO_ENCODING
|
AUDIO_ENCODING
|
||||||
)
|
)
|
||||||
if (minBufferSize == 0) {
|
if (bufferSize <= 0) {
|
||||||
Log.d(logTag, "get min buffer size fail!")
|
Log.d(logTag, "get min buffer size fail!")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
audioReader = AudioReader(minBufferSize, 4)
|
audioReader = AudioReader(bufferSize, 4)
|
||||||
|
minBufferSize = bufferSize
|
||||||
Log.d(logTag, "init audioData len:$minBufferSize")
|
Log.d(logTag, "init audioData len:$minBufferSize")
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.M)
|
private fun releaseRecorder(recorder: AudioRecord) {
|
||||||
fun startAudioRecorder() {
|
try {
|
||||||
checkAudioReader()
|
recorder.release()
|
||||||
if (audioReader != null && audioRecorder != null && minBufferSize != 0) {
|
} finally {
|
||||||
try {
|
if (audioRecorder === recorder) {
|
||||||
FFI.setFrameRawEnable("audio", true)
|
audioRecorder = null
|
||||||
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")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun captureAudio(reader: AudioReader, recorder: AudioRecord) {
|
||||||
|
try {
|
||||||
|
while (audioRecordStat) {
|
||||||
|
reader.readSync(recorder)?.let {
|
||||||
|
FFI.onAudioFrameUpdate(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
minBufferSize = 0
|
||||||
|
try {
|
||||||
|
releaseRecorder(recorder)
|
||||||
|
} finally {
|
||||||
|
releaseAudioFramePublisher()
|
||||||
|
Log.d(logTag, "Exit audio thread")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.M)
|
||||||
|
fun startAudioRecorder(): Boolean {
|
||||||
|
val recorder = audioRecorder
|
||||||
|
if (recorder == null) {
|
||||||
|
Log.d(logTag, "startAudioRecorder fail")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
var audioFramePublisherAcquired = false
|
||||||
|
return try {
|
||||||
|
checkAudioReader()
|
||||||
|
val reader = audioReader
|
||||||
|
if (reader == null || minBufferSize == 0) {
|
||||||
|
releaseRecorder(recorder)
|
||||||
|
Log.d(logTag, "startAudioRecorder fail")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
recorder.startRecording()
|
||||||
|
if (recorder.recordingState != AudioRecord.RECORDSTATE_RECORDING) {
|
||||||
|
throw IllegalStateException("AudioRecord failed to enter recording state")
|
||||||
|
}
|
||||||
|
audioRecordStat = true
|
||||||
|
val captureThread = thread(start = false) { captureAudio(reader, recorder) }
|
||||||
|
acquireAudioFramePublisher()
|
||||||
|
audioFramePublisherAcquired = true
|
||||||
|
audioThread = captureThread
|
||||||
|
captureThread.start()
|
||||||
|
true
|
||||||
|
} catch (error: Exception) {
|
||||||
|
audioRecordStat = false
|
||||||
|
audioThread = null
|
||||||
|
Log.e(logTag, "startAudioRecorder fail", error)
|
||||||
|
try {
|
||||||
|
releaseRecorder(recorder)
|
||||||
|
} finally {
|
||||||
|
if (audioFramePublisherAcquired) {
|
||||||
|
releaseAudioFramePublisher()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isVoiceCallActive(): Boolean {
|
||||||
|
return audioRecorder?.audioSource == MediaRecorder.AudioSource.VOICE_COMMUNICATION
|
||||||
|
}
|
||||||
|
|
||||||
fun onVoiceCallStarted(mediaProjection: MediaProjection?): Boolean {
|
fun onVoiceCallStarted(mediaProjection: MediaProjection?): Boolean {
|
||||||
if (!isSupportVoiceCall()) {
|
if (!isSupportVoiceCall()) {
|
||||||
return false
|
return false
|
||||||
@@ -137,11 +209,9 @@ class AudioRecordHandle(private var context: Context, private var isVideoStart:
|
|||||||
if (!isSupportVoiceCall()) {
|
if (!isSupportVoiceCall()) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
if (isVideoStart()) {
|
val switched = !isVideoStart() || switchOutVoiceCall(mediaProjection)
|
||||||
switchOutVoiceCall(mediaProjection)
|
|
||||||
}
|
|
||||||
tryReleaseAudio()
|
tryReleaseAudio()
|
||||||
return true
|
return switched
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.M)
|
@RequiresApi(Build.VERSION_CODES.M)
|
||||||
@@ -159,8 +229,7 @@ class AudioRecordHandle(private var context: Context, private var isVideoStart:
|
|||||||
Log.e(logTag, "createAudioRecorder fail")
|
Log.e(logTag, "createAudioRecorder fail")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
startAudioRecorder()
|
return startAudioRecorder()
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.M)
|
@RequiresApi(Build.VERSION_CODES.M)
|
||||||
@@ -177,8 +246,7 @@ class AudioRecordHandle(private var context: Context, private var isVideoStart:
|
|||||||
Log.e(logTag, "createAudioRecorder fail")
|
Log.e(logTag, "createAudioRecorder fail")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
startAudioRecorder()
|
return startAudioRecorder()
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun tryReleaseAudio() {
|
fun tryReleaseAudio() {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ package com.carriez.flutter_hbb
|
|||||||
|
|
||||||
import ffi.FFI
|
import ffi.FFI
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
import android.content.ComponentName
|
import android.content.ComponentName
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
@@ -24,6 +25,10 @@ import android.media.MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface
|
|||||||
import android.media.MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar
|
import android.media.MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar
|
||||||
import android.media.MediaCodecList
|
import android.media.MediaCodecList
|
||||||
import android.media.MediaFormat
|
import android.media.MediaFormat
|
||||||
|
import android.net.Uri
|
||||||
|
import android.provider.DocumentsContract
|
||||||
|
import android.provider.OpenableColumns
|
||||||
|
import android.webkit.MimeTypeMap
|
||||||
import android.util.DisplayMetrics
|
import android.util.DisplayMetrics
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import org.json.JSONArray
|
import org.json.JSONArray
|
||||||
@@ -33,6 +38,9 @@ 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
|
import kotlin.concurrent.thread
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileInputStream
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
|
||||||
|
|
||||||
class MainActivity : FlutterActivity() {
|
class MainActivity : FlutterActivity() {
|
||||||
@@ -46,6 +54,23 @@ class MainActivity : FlutterActivity() {
|
|||||||
private val channelTag = "mChannel"
|
private val channelTag = "mChannel"
|
||||||
private val logTag = "mMainActivity"
|
private val logTag = "mMainActivity"
|
||||||
private var mainService: MainService? = null
|
private var mainService: MainService? = null
|
||||||
|
private sealed class PendingPicker {
|
||||||
|
data class ImportFiles(val result: MethodChannel.Result) : PendingPicker()
|
||||||
|
data class ExportFile(val source: File, val result: MethodChannel.Result) : PendingPicker()
|
||||||
|
data class ImportDirectory(val result: MethodChannel.Result) : PendingPicker()
|
||||||
|
data class ExportFiles(
|
||||||
|
val sources: List<File>,
|
||||||
|
val rejected: Int,
|
||||||
|
val result: MethodChannel.Result
|
||||||
|
) : PendingPicker()
|
||||||
|
}
|
||||||
|
|
||||||
|
private data class ExportSource(
|
||||||
|
val file: File,
|
||||||
|
val children: List<ExportSource>?
|
||||||
|
)
|
||||||
|
|
||||||
|
private var pendingPicker: PendingPicker? = null
|
||||||
|
|
||||||
private var isAudioStart = false
|
private var isAudioStart = false
|
||||||
private val audioRecordHandle = AudioRecordHandle(this, { false }, { isAudioStart })
|
private val audioRecordHandle = AudioRecordHandle(this, { false }, { isAudioStart })
|
||||||
@@ -91,6 +116,108 @@ class MainActivity : FlutterActivity() {
|
|||||||
|
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
super.onActivityResult(requestCode, resultCode, data)
|
super.onActivityResult(requestCode, resultCode, data)
|
||||||
|
if (requestCode == REQ_IMPORT_FILES) {
|
||||||
|
val pending = pendingPicker as? PendingPicker.ImportFiles ?: return
|
||||||
|
pendingPicker = null
|
||||||
|
if (resultCode != Activity.RESULT_OK || data == null) {
|
||||||
|
pending.result.success(emptyList<Map<String, String>>())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val uris = linkedSetOf<Uri>()
|
||||||
|
data.data?.let { uris.add(it) }
|
||||||
|
data.clipData?.let { clipData ->
|
||||||
|
for (index in 0 until clipData.itemCount) {
|
||||||
|
uris.add(clipData.getItemAt(index).uri)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
thread {
|
||||||
|
val files = uris.map { uri ->
|
||||||
|
mapOf(
|
||||||
|
"uri" to uri.toString(),
|
||||||
|
"name" to (displayName(uri) ?: uri.lastPathSegment.orEmpty())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
runOnUiThread { pending.result.success(files) }
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (requestCode == REQ_EXPORT_FILE) {
|
||||||
|
val pending = pendingPicker as? PendingPicker.ExportFile ?: return
|
||||||
|
pendingPicker = null
|
||||||
|
val destination = data?.data
|
||||||
|
|
||||||
|
if (resultCode != Activity.RESULT_OK || destination == null) {
|
||||||
|
pending.result.success(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
thread {
|
||||||
|
try {
|
||||||
|
FileInputStream(pending.source).use { input ->
|
||||||
|
contentResolver.openOutputStream(destination, "wt")?.use { output ->
|
||||||
|
input.copyTo(output)
|
||||||
|
} ?: throw IllegalStateException("Unable to open the selected destination")
|
||||||
|
}
|
||||||
|
runOnUiThread { pending.result.success(true) }
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(logTag, "Failed to export file", e)
|
||||||
|
runOnUiThread {
|
||||||
|
pending.result.error("export_failed", e.message, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (requestCode == REQ_IMPORT_DIRECTORY) {
|
||||||
|
val pending = pendingPicker as? PendingPicker.ImportDirectory ?: return
|
||||||
|
pendingPicker = null
|
||||||
|
val treeUri = data?.data
|
||||||
|
if (resultCode != Activity.RESULT_OK || treeUri == null) {
|
||||||
|
pending.result.success(null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
thread {
|
||||||
|
val selected = mapOf(
|
||||||
|
"uri" to treeUri.toString(),
|
||||||
|
"name" to (treeDisplayName(treeUri) ?: "Imported")
|
||||||
|
)
|
||||||
|
runOnUiThread { pending.result.success(selected) }
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (requestCode == REQ_EXPORT_FILES) {
|
||||||
|
val pending = pendingPicker as? PendingPicker.ExportFiles ?: return
|
||||||
|
pendingPicker = null
|
||||||
|
val treeUri = data?.data
|
||||||
|
if (resultCode != Activity.RESULT_OK || treeUri == null) {
|
||||||
|
pending.result.success(null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
thread {
|
||||||
|
var exported = 0
|
||||||
|
var failed = pending.rejected
|
||||||
|
var processed = 0
|
||||||
|
try {
|
||||||
|
val sources = pending.sources.map { snapshotExportSource(it) }
|
||||||
|
val rootDocId = DocumentsContract.getTreeDocumentId(treeUri)
|
||||||
|
sources.forEach { source ->
|
||||||
|
val ok = source?.let {
|
||||||
|
copyExportSourceToTree(treeUri, rootDocId, it)
|
||||||
|
} ?: false
|
||||||
|
if (ok) exported++ else failed++
|
||||||
|
processed++
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(logTag, "Failed to export selected files", e)
|
||||||
|
failed += pending.sources.size - processed
|
||||||
|
}
|
||||||
|
runOnUiThread {
|
||||||
|
pending.result.success(mapOf("exported" to exported, "failed" to failed))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
if (requestCode == REQ_INVOKE_PERMISSION_ACTIVITY_MEDIA_PROJECTION && resultCode == RES_FAILED) {
|
if (requestCode == REQ_INVOKE_PERMISSION_ACTIVITY_MEDIA_PROJECTION && resultCode == RES_FAILED) {
|
||||||
flutterMethodChannel?.invokeMethod("on_media_projection_canceled", null)
|
flutterMethodChannel?.invokeMethod("on_media_projection_canceled", null)
|
||||||
}
|
}
|
||||||
@@ -267,6 +394,242 @@ class MainActivity : FlutterActivity() {
|
|||||||
result.success(false)
|
result.success(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
PICK_IMPORT_FILES -> {
|
||||||
|
if (pendingPicker != null) {
|
||||||
|
result.error("picker_in_progress", "Another document picker is already open", null)
|
||||||
|
} else {
|
||||||
|
pendingPicker = PendingPicker.ImportFiles(result)
|
||||||
|
try {
|
||||||
|
startActivityForResult(
|
||||||
|
Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
|
||||||
|
addCategory(Intent.CATEGORY_OPENABLE)
|
||||||
|
type = "*/*"
|
||||||
|
putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
|
||||||
|
},
|
||||||
|
REQ_IMPORT_FILES
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
pendingPicker = null
|
||||||
|
result.error("picker_unavailable", e.message, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
IMPORT_FILE -> {
|
||||||
|
val arguments = call.arguments as? Map<*, *>
|
||||||
|
val uri = (arguments?.get("uri") as? String)?.let {
|
||||||
|
runCatching { Uri.parse(it) }.getOrNull()
|
||||||
|
}
|
||||||
|
val path = arguments?.get("path") as? String
|
||||||
|
val overwrite = arguments?.get("overwrite") as? Boolean ?: false
|
||||||
|
val destination = path?.let { canonicalAppScopedFile(it) }
|
||||||
|
|
||||||
|
if (uri?.scheme != "content") {
|
||||||
|
result.error("invalid_uri", "The selected document URI is invalid", null)
|
||||||
|
} else if (destination == null ||
|
||||||
|
destination.isDirectory ||
|
||||||
|
destination.parentFile?.isDirectory != true) {
|
||||||
|
result.error("invalid_destination", "The destination is outside app-scoped storage", null)
|
||||||
|
} else {
|
||||||
|
thread {
|
||||||
|
var temporary: File? = null
|
||||||
|
var reservedDestination = false
|
||||||
|
var errorCode = "import_failed"
|
||||||
|
try {
|
||||||
|
val temporaryFile = File.createTempFile(
|
||||||
|
".rustdesk-import-",
|
||||||
|
".tmp",
|
||||||
|
destination.parentFile
|
||||||
|
)
|
||||||
|
temporary = temporaryFile
|
||||||
|
contentResolver.openInputStream(uri)?.use { input ->
|
||||||
|
FileOutputStream(temporaryFile).use { output ->
|
||||||
|
input.copyTo(output)
|
||||||
|
}
|
||||||
|
} ?: throw IllegalStateException("Unable to open the selected document")
|
||||||
|
if (!overwrite) {
|
||||||
|
reservedDestination = destination.createNewFile()
|
||||||
|
if (!reservedDestination) {
|
||||||
|
throw IllegalStateException("The destination already exists")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!temporaryFile.renameTo(destination)) {
|
||||||
|
if (reservedDestination) {
|
||||||
|
destination.delete()
|
||||||
|
}
|
||||||
|
errorCode = "rename_failed"
|
||||||
|
throw IllegalStateException("Unable to replace the destination")
|
||||||
|
}
|
||||||
|
runOnUiThread { result.success(true) }
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(logTag, "Failed to import file", e)
|
||||||
|
runOnUiThread {
|
||||||
|
result.error(errorCode, e.message, null)
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
temporary?.delete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EXPORT_FILE -> {
|
||||||
|
val path = (call.arguments as? Map<*, *>)?.get("path") as? String
|
||||||
|
val source = path?.let { canonicalExportSource(it) }
|
||||||
|
|
||||||
|
if (source?.isFile != true) {
|
||||||
|
result.error("invalid_source", "The file is outside app-scoped storage", null)
|
||||||
|
} else if (pendingPicker != null) {
|
||||||
|
result.error("picker_in_progress", "Another document picker is already open", null)
|
||||||
|
} else {
|
||||||
|
val mimeType = MimeTypeMap.getSingleton()
|
||||||
|
.getMimeTypeFromExtension(source.extension.lowercase())
|
||||||
|
?: "application/octet-stream"
|
||||||
|
pendingPicker = PendingPicker.ExportFile(source, result)
|
||||||
|
try {
|
||||||
|
startActivityForResult(
|
||||||
|
Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
|
||||||
|
addCategory(Intent.CATEGORY_OPENABLE)
|
||||||
|
type = mimeType
|
||||||
|
putExtra(Intent.EXTRA_TITLE, source.name)
|
||||||
|
},
|
||||||
|
REQ_EXPORT_FILE
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
pendingPicker = null
|
||||||
|
result.error("picker_unavailable", e.message, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PICK_IMPORT_DIRECTORY -> {
|
||||||
|
if (pendingPicker != null) {
|
||||||
|
result.error("picker_in_progress", "Another document picker is already open", null)
|
||||||
|
} else {
|
||||||
|
pendingPicker = PendingPicker.ImportDirectory(result)
|
||||||
|
try {
|
||||||
|
startActivityForResult(
|
||||||
|
Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).apply {
|
||||||
|
putExtra(Intent.EXTRA_TITLE, "Select the folder to import")
|
||||||
|
},
|
||||||
|
REQ_IMPORT_DIRECTORY
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
pendingPicker = null
|
||||||
|
result.error("picker_unavailable", e.message, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
IMPORT_DIRECTORY -> {
|
||||||
|
val arguments = call.arguments as? Map<*, *>
|
||||||
|
val uri = (arguments?.get("uri") as? String)?.let {
|
||||||
|
runCatching { Uri.parse(it) }.getOrNull()
|
||||||
|
}
|
||||||
|
val path = arguments?.get("path") as? String
|
||||||
|
val overwrite = arguments?.get("overwrite") as? Boolean ?: false
|
||||||
|
val destination = path?.let { canonicalAppScopedFile(it) }
|
||||||
|
|
||||||
|
if (uri?.scheme != "content") {
|
||||||
|
result.error("invalid_uri", "The selected document URI is invalid", null)
|
||||||
|
} else if (destination == null ||
|
||||||
|
destination.parentFile?.isDirectory != true ||
|
||||||
|
(destination.exists() && !destination.isDirectory)) {
|
||||||
|
result.error("invalid_destination", "The destination is outside app-scoped storage", null)
|
||||||
|
} else {
|
||||||
|
thread {
|
||||||
|
var temporary: File? = null
|
||||||
|
var backup: File? = null
|
||||||
|
val ok = try {
|
||||||
|
val parent = destination.parentFile
|
||||||
|
?: throw IllegalStateException("The destination has no parent")
|
||||||
|
temporary = File.createTempFile(
|
||||||
|
".rustdesk-import-dir-",
|
||||||
|
".tmp",
|
||||||
|
parent
|
||||||
|
).also {
|
||||||
|
if (!it.delete() || !it.mkdir()) {
|
||||||
|
throw IllegalStateException("Unable to create a temporary folder")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!copyDocumentTreeToFile(uri, temporary!!)) {
|
||||||
|
throw IllegalStateException("Unable to read all folder contents")
|
||||||
|
}
|
||||||
|
if (destination.exists()) {
|
||||||
|
if (!overwrite) {
|
||||||
|
throw IllegalStateException("The destination already exists")
|
||||||
|
}
|
||||||
|
val backupFile = File.createTempFile(
|
||||||
|
".rustdesk-import-backup-",
|
||||||
|
".tmp",
|
||||||
|
parent
|
||||||
|
)
|
||||||
|
if (!backupFile.delete()) {
|
||||||
|
throw IllegalStateException("Unable to prepare the destination backup")
|
||||||
|
}
|
||||||
|
backup = backupFile
|
||||||
|
if (!destination.renameTo(backupFile)) {
|
||||||
|
throw IllegalStateException("Unable to replace the destination")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!temporary!!.renameTo(destination)) {
|
||||||
|
val destinationBackup = backup
|
||||||
|
if (destinationBackup != null &&
|
||||||
|
!destinationBackup.renameTo(destination)
|
||||||
|
) {
|
||||||
|
throw IllegalStateException(
|
||||||
|
"Unable to move the imported folder and restore " +
|
||||||
|
"the destination from $destinationBackup"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
throw IllegalStateException("Unable to move the imported folder")
|
||||||
|
}
|
||||||
|
temporary = null
|
||||||
|
val destinationBackup = backup
|
||||||
|
if (destinationBackup != null &&
|
||||||
|
!destinationBackup.deleteRecursively()
|
||||||
|
) {
|
||||||
|
throw IllegalStateException(
|
||||||
|
"Unable to remove the destination backup: $destinationBackup"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
backup = null
|
||||||
|
true
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(logTag, "Failed to import directory", e)
|
||||||
|
false
|
||||||
|
} finally {
|
||||||
|
temporary?.deleteRecursively()
|
||||||
|
}
|
||||||
|
runOnUiThread { result.success(ok) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EXPORT_FILES -> {
|
||||||
|
val paths = (call.arguments as? Map<*, *>)?.get("paths") as? List<*>
|
||||||
|
if (paths.isNullOrEmpty()) {
|
||||||
|
result.error("invalid_source", "The selected files are outside app-scoped storage", null)
|
||||||
|
} else {
|
||||||
|
val sources = paths.mapNotNull {
|
||||||
|
(it as? String)?.let(::canonicalExportSource)
|
||||||
|
}
|
||||||
|
val rejected = paths.size - sources.size
|
||||||
|
if (sources.isEmpty()) {
|
||||||
|
result.success(mapOf("exported" to 0, "failed" to rejected))
|
||||||
|
} else if (pendingPicker != null) {
|
||||||
|
result.error("picker_in_progress", "Another document picker is already open", null)
|
||||||
|
} else {
|
||||||
|
pendingPicker = PendingPicker.ExportFiles(sources, rejected, result)
|
||||||
|
try {
|
||||||
|
startActivityForResult(
|
||||||
|
Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).apply {
|
||||||
|
putExtra(Intent.EXTRA_TITLE, "Select the destination folder")
|
||||||
|
},
|
||||||
|
REQ_EXPORT_FILES
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
pendingPicker = null
|
||||||
|
result.error("picker_unavailable", e.message, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
GET_VALUE -> {
|
GET_VALUE -> {
|
||||||
if (call.arguments is String) {
|
if (call.arguments is String) {
|
||||||
if (call.arguments == KEY_IS_SUPPORT_VOICE_CALL) {
|
if (call.arguments == KEY_IS_SUPPORT_VOICE_CALL) {
|
||||||
@@ -291,6 +654,228 @@ class MainActivity : FlutterActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun canonicalAppScopedFile(path: String): File? {
|
||||||
|
val file = runCatching { File(path).canonicalFile }.getOrNull() ?: return null
|
||||||
|
val allowedRoots = listOfNotNull(filesDir, getExternalFilesDir(null)).mapNotNull {
|
||||||
|
runCatching { it.canonicalFile }.getOrNull()
|
||||||
|
}
|
||||||
|
return file.takeIf { candidate ->
|
||||||
|
allowedRoots.any { root ->
|
||||||
|
candidate == root || candidate.path.startsWith(root.path + File.separator)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun canonicalExportSource(path: String): File? {
|
||||||
|
val original = File(path).absoluteFile
|
||||||
|
val canonical = canonicalAppScopedFile(path) ?: return null
|
||||||
|
return canonical.takeIf {
|
||||||
|
original.path == canonical.path && (canonical.isFile || canonical.isDirectory)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun snapshotExportSource(source: File): ExportSource? {
|
||||||
|
val safeSource = canonicalExportSource(source.path) ?: return null
|
||||||
|
if (safeSource.isFile) return ExportSource(safeSource, null)
|
||||||
|
val sourceChildren = safeSource.listFiles() ?: return null
|
||||||
|
val children = ArrayList<ExportSource>(sourceChildren.size)
|
||||||
|
for (child in sourceChildren) {
|
||||||
|
val snapshot = snapshotExportSource(child) ?: return null
|
||||||
|
children.add(snapshot)
|
||||||
|
}
|
||||||
|
return ExportSource(safeSource, children)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun copyExportSourceToTree(
|
||||||
|
treeUri: Uri,
|
||||||
|
parentDocId: String,
|
||||||
|
source: ExportSource
|
||||||
|
): Boolean {
|
||||||
|
val children = source.children
|
||||||
|
return if (children == null) {
|
||||||
|
copyFileToTree(treeUri, parentDocId, source.file)
|
||||||
|
} else {
|
||||||
|
copyDirToTree(treeUri, parentDocId, source)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun treeDisplayName(treeUri: Uri): String? {
|
||||||
|
return try {
|
||||||
|
val rootDocId = DocumentsContract.getTreeDocumentId(treeUri)
|
||||||
|
val docUri = DocumentsContract.buildDocumentUriUsingTree(treeUri, rootDocId)
|
||||||
|
contentResolver.query(
|
||||||
|
docUri,
|
||||||
|
arrayOf(DocumentsContract.Document.COLUMN_DISPLAY_NAME),
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
)?.use { cursor -> if (cursor.moveToFirst()) cursor.getString(0) else null }
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(logTag, "Failed to read selected folder name", e)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun copyDocumentTreeToFile(treeUri: Uri, destinationDir: File): Boolean {
|
||||||
|
val rootDocId = DocumentsContract.getTreeDocumentId(treeUri)
|
||||||
|
return copyChildrenToFile(treeUri, rootDocId, destinationDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun copyChildrenToFile(
|
||||||
|
treeUri: Uri,
|
||||||
|
parentDocId: String,
|
||||||
|
destinationDir: File
|
||||||
|
): Boolean {
|
||||||
|
val childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(treeUri, parentDocId)
|
||||||
|
var ok = true
|
||||||
|
val destinationNames = HashSet<String>()
|
||||||
|
val cursor = contentResolver.query(childrenUri, childColumns, null, null, null)
|
||||||
|
?: return false
|
||||||
|
cursor.use {
|
||||||
|
while (cursor.moveToNext()) {
|
||||||
|
val docId = cursor.getString(0)
|
||||||
|
val name = cursor.getString(1)
|
||||||
|
val mime = cursor.getString(2)
|
||||||
|
val docUri = DocumentsContract.buildDocumentUriUsingTree(treeUri, docId)
|
||||||
|
if (name != null && !destinationNames.add(name)) {
|
||||||
|
ok = false
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
val destination = safeDestinationChild(destinationDir, name)
|
||||||
|
if (destination == null || destination.exists()) {
|
||||||
|
ok = false
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (mime == DocumentsContract.Document.MIME_TYPE_DIR) {
|
||||||
|
if (!destination.mkdirs() && !destination.isDirectory) {
|
||||||
|
ok = false
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (!copyChildrenToFile(treeUri, docId, destination)) {
|
||||||
|
ok = false
|
||||||
|
}
|
||||||
|
} else if (!copyDocumentToFile(docUri, destination)) {
|
||||||
|
ok = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun safeDestinationChild(destinationDir: File, name: String?): File? {
|
||||||
|
if (name.isNullOrEmpty() || name == "." || name == ".." ||
|
||||||
|
name.indexOf('\u0000') >= 0 || name.contains('/') || name.contains('\\')) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
val parent = runCatching { destinationDir.canonicalFile }.getOrNull() ?: return null
|
||||||
|
val child = runCatching { File(parent, name).canonicalFile }.getOrNull() ?: return null
|
||||||
|
return child.takeIf { it.path.startsWith(parent.path + File.separator) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun copyDocumentToFile(uri: Uri, destination: File): Boolean {
|
||||||
|
return try {
|
||||||
|
destination.parentFile?.mkdirs()
|
||||||
|
if (destination.exists() && !destination.delete()) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
contentResolver.openInputStream(uri)?.use { input ->
|
||||||
|
FileOutputStream(destination).use { output -> input.copyTo(output) }
|
||||||
|
} != null
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(logTag, "Failed to copy document to $destination", e)
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun copyFileToTree(treeUri: Uri, parentDocId: String, source: File): Boolean {
|
||||||
|
val safeSource = canonicalExportSource(source.path)?.takeIf { it.isFile } ?: return false
|
||||||
|
return try {
|
||||||
|
val mime = MimeTypeMap.getSingleton()
|
||||||
|
.getMimeTypeFromExtension(safeSource.extension.lowercase())
|
||||||
|
?: "application/octet-stream"
|
||||||
|
val parentUri = DocumentsContract.buildDocumentUriUsingTree(treeUri, parentDocId)
|
||||||
|
val docUri = DocumentsContract.createDocument(
|
||||||
|
contentResolver,
|
||||||
|
parentUri,
|
||||||
|
mime,
|
||||||
|
safeSource.name
|
||||||
|
) ?: return false
|
||||||
|
contentResolver.openOutputStream(docUri, "wt")?.use { output ->
|
||||||
|
FileInputStream(safeSource).use { input -> input.copyTo(output) }
|
||||||
|
} ?: return false
|
||||||
|
true
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(logTag, "Failed to export file $safeSource", e)
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun copyDirToTree(
|
||||||
|
treeUri: Uri,
|
||||||
|
parentDocId: String,
|
||||||
|
source: ExportSource
|
||||||
|
): Boolean {
|
||||||
|
val children = source.children ?: return false
|
||||||
|
val safeSource = canonicalExportSource(source.file.path)?.takeIf { it.isDirectory }
|
||||||
|
?: return false
|
||||||
|
val parentUri = DocumentsContract.buildDocumentUriUsingTree(treeUri, parentDocId)
|
||||||
|
var dirDocId = findChildDocId(treeUri, parentDocId, safeSource.name)
|
||||||
|
if (dirDocId == null) {
|
||||||
|
dirDocId = try {
|
||||||
|
DocumentsContract.createDocument(
|
||||||
|
contentResolver,
|
||||||
|
parentUri,
|
||||||
|
DocumentsContract.Document.MIME_TYPE_DIR,
|
||||||
|
safeSource.name
|
||||||
|
)?.let { DocumentsContract.getDocumentId(it) }
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(logTag, "Failed to create folder ${safeSource.name}", e)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (dirDocId == null) return false
|
||||||
|
|
||||||
|
var ok = true
|
||||||
|
children.forEach { child ->
|
||||||
|
val childOk = copyExportSourceToTree(treeUri, dirDocId, child)
|
||||||
|
if (!childOk) ok = false
|
||||||
|
}
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun findChildDocId(treeUri: Uri, parentDocId: String, name: String): String? {
|
||||||
|
val childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(treeUri, parentDocId)
|
||||||
|
val cursor = contentResolver.query(childrenUri, childColumns, null, null, null)
|
||||||
|
?: throw IllegalStateException("Unable to query destination folder")
|
||||||
|
cursor.use {
|
||||||
|
while (cursor.moveToNext()) {
|
||||||
|
if (cursor.getString(1) == name &&
|
||||||
|
cursor.getString(2) == DocumentsContract.Document.MIME_TYPE_DIR
|
||||||
|
) {
|
||||||
|
return cursor.getString(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
private val childColumns = arrayOf(
|
||||||
|
DocumentsContract.Document.COLUMN_DOCUMENT_ID,
|
||||||
|
DocumentsContract.Document.COLUMN_DISPLAY_NAME,
|
||||||
|
DocumentsContract.Document.COLUMN_MIME_TYPE
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun displayName(uri: Uri): String? {
|
||||||
|
return try {
|
||||||
|
contentResolver.query(uri, arrayOf(OpenableColumns.DISPLAY_NAME), null, null, null)?.use { cursor ->
|
||||||
|
if (cursor.moveToFirst()) cursor.getString(0) else null
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(logTag, "Failed to read selected document name", e)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun setCodecInfo() {
|
private fun setCodecInfo() {
|
||||||
val codecList = MediaCodecList(MediaCodecList.REGULAR_CODECS)
|
val codecList = MediaCodecList(MediaCodecList.REGULAR_CODECS)
|
||||||
val codecs = codecList.codecInfos
|
val codecs = codecList.codecInfos
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import android.app.PendingIntent.FLAG_UPDATE_CURRENT
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
|
import android.content.pm.ServiceInfo
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import android.content.res.Configuration.ORIENTATION_LANDSCAPE
|
import android.content.res.Configuration.ORIENTATION_LANDSCAPE
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
@@ -150,7 +151,7 @@ class MainService : Service() {
|
|||||||
if (incomingVoiceCall) {
|
if (incomingVoiceCall) {
|
||||||
voiceCallRequestNotification(id, "Voice Call Request", username, peerId)
|
voiceCallRequestNotification(id, "Voice Call Request", username, peerId)
|
||||||
} else {
|
} else {
|
||||||
if (!audioRecordHandle.switchOutVoiceCall(mediaProjection)) {
|
if (!switchOutVoiceCall()) {
|
||||||
Log.e(logTag, "switchOutVoiceCall fail")
|
Log.e(logTag, "switchOutVoiceCall fail")
|
||||||
MainActivity.flutterMethodChannel?.invokeMethod("msgbox", mapOf(
|
MainActivity.flutterMethodChannel?.invokeMethod("msgbox", mapOf(
|
||||||
"type" to "custom-nook-nocancel-hasclose-error",
|
"type" to "custom-nook-nocancel-hasclose-error",
|
||||||
@@ -159,7 +160,7 @@ class MainService : Service() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (!audioRecordHandle.switchToVoiceCall(mediaProjection)) {
|
if (!switchToVoiceCall()) {
|
||||||
Log.e(logTag, "switchToVoiceCall fail")
|
Log.e(logTag, "switchToVoiceCall fail")
|
||||||
MainActivity.flutterMethodChannel?.invokeMethod("msgbox", mapOf(
|
MainActivity.flutterMethodChannel?.invokeMethod("msgbox", mapOf(
|
||||||
"type" to "custom-nook-nocancel-hasclose-error",
|
"type" to "custom-nook-nocancel-hasclose-error",
|
||||||
@@ -214,6 +215,19 @@ class MainService : Service() {
|
|||||||
|
|
||||||
// video
|
// video
|
||||||
private var mediaProjection: MediaProjection? = null
|
private var mediaProjection: MediaProjection? = null
|
||||||
|
private var mediaProjectionCallback: MediaProjection.Callback? = null
|
||||||
|
private var captureRestartPending = false
|
||||||
|
private var captureRestartInVoiceCall = false
|
||||||
|
private val mediaProjectionResultReceiver =
|
||||||
|
object : ResultReceiver(Handler(Looper.getMainLooper())) {
|
||||||
|
override fun onReceiveResult(resultCode: Int, resultData: Bundle?) {
|
||||||
|
if (resultCode == RES_FAILED) {
|
||||||
|
cancelMediaProjectionRecovery()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private var mediaProjectionForegroundService = false
|
||||||
|
private var microphoneForegroundService = false
|
||||||
private var surface: Surface? = null
|
private var surface: Surface? = null
|
||||||
private val sendVP9Thread = Executors.newSingleThreadExecutor()
|
private val sendVP9Thread = Executors.newSingleThreadExecutor()
|
||||||
private var videoEncoder: MediaCodec? = null
|
private var videoEncoder: MediaCodec? = null
|
||||||
@@ -243,7 +257,9 @@ class MainService : Service() {
|
|||||||
// keep the config dir same with flutter
|
// keep the config dir same with flutter
|
||||||
val prefs = applicationContext.getSharedPreferences(KEY_SHARED_PREFERENCES, FlutterActivity.MODE_PRIVATE)
|
val prefs = applicationContext.getSharedPreferences(KEY_SHARED_PREFERENCES, FlutterActivity.MODE_PRIVATE)
|
||||||
val configPath = prefs.getString(KEY_APP_DIR_CONFIG_PATH, "") ?: ""
|
val configPath = prefs.getString(KEY_APP_DIR_CONFIG_PATH, "") ?: ""
|
||||||
FFI.startServer(configPath, "")
|
val homePath = applicationContext.getExternalFilesDir(null)?.absolutePath
|
||||||
|
?: applicationContext.filesDir.absolutePath
|
||||||
|
FFI.startServer(configPath, homePath, "")
|
||||||
|
|
||||||
createForegroundNotification()
|
createForegroundNotification()
|
||||||
}
|
}
|
||||||
@@ -337,8 +353,6 @@ class MainService : Service() {
|
|||||||
Log.d("whichService", "this service: ${Thread.currentThread()}")
|
Log.d("whichService", "this service: ${Thread.currentThread()}")
|
||||||
super.onStartCommand(intent, flags, startId)
|
super.onStartCommand(intent, flags, startId)
|
||||||
if (intent?.action == ACT_INIT_MEDIA_PROJECTION_AND_SERVICE) {
|
if (intent?.action == ACT_INIT_MEDIA_PROJECTION_AND_SERVICE) {
|
||||||
createForegroundNotification()
|
|
||||||
|
|
||||||
if (intent.getBooleanExtra(EXT_INIT_FROM_BOOT, false)) {
|
if (intent.getBooleanExtra(EXT_INIT_FROM_BOOT, false)) {
|
||||||
FFI.startService()
|
FFI.startService()
|
||||||
}
|
}
|
||||||
@@ -347,10 +361,7 @@ class MainService : Service() {
|
|||||||
getSystemService(MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
|
getSystemService(MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
|
||||||
|
|
||||||
intent.getParcelableExtra<Intent>(EXT_MEDIA_PROJECTION_RES_INTENT)?.let {
|
intent.getParcelableExtra<Intent>(EXT_MEDIA_PROJECTION_RES_INTENT)?.let {
|
||||||
mediaProjection =
|
replaceMediaProjection(mediaProjectionManager, it)
|
||||||
mediaProjectionManager.getMediaProjection(Activity.RESULT_OK, it)
|
|
||||||
checkMediaPermission()
|
|
||||||
_isReady = true
|
|
||||||
} ?: let {
|
} ?: let {
|
||||||
Log.d(logTag, "getParcelableExtra intent null, invoke requestMediaProjection")
|
Log.d(logTag, "getParcelableExtra intent null, invoke requestMediaProjection")
|
||||||
requestMediaProjection()
|
requestMediaProjection()
|
||||||
@@ -364,14 +375,23 @@ class MainService : Service() {
|
|||||||
updateScreenInfo(newConfig.orientation)
|
updateScreenInfo(newConfig.orientation)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun requestMediaProjection() {
|
private fun requestMediaProjection(recovery: Boolean = false) {
|
||||||
val intent = Intent(this, PermissionRequestTransparentActivity::class.java).apply {
|
val intent = Intent(this, PermissionRequestTransparentActivity::class.java).apply {
|
||||||
action = ACT_REQUEST_MEDIA_PROJECTION
|
action = ACT_REQUEST_MEDIA_PROJECTION
|
||||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||||
|
if (recovery) {
|
||||||
|
putExtra(EXT_MEDIA_PROJECTION_RESULT_RECEIVER, mediaProjectionResultReceiver)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
private fun cancelMediaProjectionRecovery() {
|
||||||
|
captureRestartPending = false
|
||||||
|
captureRestartInVoiceCall = false
|
||||||
|
}
|
||||||
|
|
||||||
@SuppressLint("WrongConstant")
|
@SuppressLint("WrongConstant")
|
||||||
private fun createSurface(): Surface? {
|
private fun createSurface(): Surface? {
|
||||||
return if (useVP9) {
|
return if (useVP9) {
|
||||||
@@ -405,15 +425,149 @@ class MainService : Service() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onVoiceCallStarted(): Boolean {
|
private fun releaseMediaProjection() {
|
||||||
return audioRecordHandle.onVoiceCallStarted(mediaProjection)
|
val projection = mediaProjection
|
||||||
|
val callback = mediaProjectionCallback
|
||||||
|
mediaProjection = null
|
||||||
|
mediaProjectionCallback = null
|
||||||
|
if (projection != null && callback != null) {
|
||||||
|
projection.unregisterCallback(callback)
|
||||||
|
}
|
||||||
|
projection?.stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
private fun handleMediaProjectionStopped(stoppedProjection: MediaProjection) {
|
||||||
|
if (mediaProjection !== stoppedProjection) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
Log.d(logTag, "MediaProjection stopped")
|
||||||
|
setMediaProjectionForegroundService(false)
|
||||||
|
stopCapture()
|
||||||
|
virtualDisplay?.release()
|
||||||
|
virtualDisplay = null
|
||||||
|
mediaProjection = null
|
||||||
|
mediaProjectionCallback = null
|
||||||
|
_isReady = false
|
||||||
|
checkMediaPermission()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
private fun replaceMediaProjection(
|
||||||
|
mediaProjectionManager: MediaProjectionManager,
|
||||||
|
resultIntent: Intent,
|
||||||
|
) {
|
||||||
|
val wasCapturing = isStart
|
||||||
|
val restartCapture = wasCapturing || captureRestartPending
|
||||||
|
val restartInVoiceCall = if (wasCapturing) {
|
||||||
|
audioRecordHandle.isVoiceCallActive()
|
||||||
|
} else {
|
||||||
|
captureRestartInVoiceCall
|
||||||
|
}
|
||||||
|
val hadProjection = mediaProjection != null
|
||||||
|
if (!setMediaProjectionForegroundService(true)) {
|
||||||
|
if (!hadProjection) {
|
||||||
|
cancelMediaProjectionRecovery()
|
||||||
|
_isReady = false
|
||||||
|
checkMediaPermission()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val projection =
|
||||||
|
mediaProjectionManager.getMediaProjection(Activity.RESULT_OK, resultIntent)
|
||||||
|
if (projection == null) {
|
||||||
|
if (!hadProjection) {
|
||||||
|
cancelMediaProjectionRecovery()
|
||||||
|
_isReady = false
|
||||||
|
setMediaProjectionForegroundService(false)
|
||||||
|
checkMediaPermission()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (wasCapturing) {
|
||||||
|
stopCapture()
|
||||||
|
}
|
||||||
|
captureRestartPending = restartCapture
|
||||||
|
virtualDisplay?.release()
|
||||||
|
virtualDisplay = null
|
||||||
|
releaseMediaProjection()
|
||||||
|
val callback = object : MediaProjection.Callback() {
|
||||||
|
override fun onStop() {
|
||||||
|
handleMediaProjectionStopped(projection)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
projection.registerCallback(callback, Handler(Looper.getMainLooper()))
|
||||||
|
mediaProjection = projection
|
||||||
|
mediaProjectionCallback = callback
|
||||||
|
_isReady = true
|
||||||
|
checkMediaPermission()
|
||||||
|
if (restartCapture) {
|
||||||
|
captureRestartPending = false
|
||||||
|
startCapture(restartInVoiceCall)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
private fun startMicrophoneCapture(startAudio: () -> Boolean): Boolean {
|
||||||
|
if (!setMicrophoneForegroundService(true)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (startAudio()) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
setMicrophoneForegroundService(false)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
private fun stopMicrophoneCapture(stopAudio: () -> Boolean): Boolean {
|
||||||
|
val stopped = stopAudio()
|
||||||
|
val foregroundServiceUpdated = setMicrophoneForegroundService(false)
|
||||||
|
return stopped && foregroundServiceUpdated
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
private fun switchToVoiceCall(): Boolean {
|
||||||
|
if (captureRestartPending) {
|
||||||
|
captureRestartInVoiceCall = true
|
||||||
|
}
|
||||||
|
return startMicrophoneCapture {
|
||||||
|
audioRecordHandle.switchToVoiceCall(mediaProjection)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
private fun switchOutVoiceCall(): Boolean {
|
||||||
|
captureRestartInVoiceCall = false
|
||||||
|
val switched = audioRecordHandle.switchOutVoiceCall(mediaProjection)
|
||||||
|
val foregroundServiceUpdated = setMicrophoneForegroundService(false)
|
||||||
|
return switched && foregroundServiceUpdated
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun onVoiceCallStarted(): Boolean {
|
||||||
|
if (captureRestartPending) {
|
||||||
|
captureRestartInVoiceCall = true
|
||||||
|
}
|
||||||
|
return startMicrophoneCapture {
|
||||||
|
audioRecordHandle.onVoiceCallStarted(mediaProjection)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
fun onVoiceCallClosed(): Boolean {
|
fun onVoiceCallClosed(): Boolean {
|
||||||
return audioRecordHandle.onVoiceCallClosed(mediaProjection)
|
captureRestartInVoiceCall = false
|
||||||
|
return stopMicrophoneCapture {
|
||||||
|
audioRecordHandle.onVoiceCallClosed(mediaProjection)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startCapture(): Boolean {
|
fun startCapture(): Boolean {
|
||||||
|
return startCapture(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
private fun startCapture(inVoiceCall: Boolean): Boolean {
|
||||||
if (isStart) {
|
if (isStart) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -421,25 +575,35 @@ class MainService : Service() {
|
|||||||
Log.w(logTag, "startCapture fail,mediaProjection is null")
|
Log.w(logTag, "startCapture fail,mediaProjection is null")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
captureRestartInVoiceCall = inVoiceCall
|
||||||
|
|
||||||
updateScreenInfo(resources.configuration.orientation)
|
updateScreenInfo(resources.configuration.orientation)
|
||||||
Log.d(logTag, "Start Capture")
|
Log.d(logTag, "Start Capture")
|
||||||
surface = createSurface()
|
surface = createSurface()
|
||||||
|
|
||||||
if (useVP9) {
|
val videoStarted = if (useVP9) {
|
||||||
startVP9VideoRecorder(mediaProjection!!)
|
startVP9VideoRecorder(mediaProjection!!)
|
||||||
} else {
|
} else {
|
||||||
startRawVideoRecorder(mediaProjection!!)
|
startRawVideoRecorder(mediaProjection!!)
|
||||||
}
|
}
|
||||||
|
if (!videoStarted) {
|
||||||
|
if (!captureRestartPending) {
|
||||||
|
captureRestartInVoiceCall = false
|
||||||
|
}
|
||||||
|
releaseFailedVideoCapture()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
if (!audioRecordHandle.createAudioRecorder(false, mediaProjection)) {
|
val audioStarted = if (inVoiceCall) {
|
||||||
Log.d(logTag, "createAudioRecorder fail")
|
switchToVoiceCall()
|
||||||
} else {
|
} else {
|
||||||
Log.d(logTag, "audio recorder start")
|
audioRecordHandle.createAudioRecorder(false, mediaProjection) &&
|
||||||
audioRecordHandle.startAudioRecorder()
|
audioRecordHandle.startAudioRecorder()
|
||||||
}
|
}
|
||||||
|
Log.d(logTag, if (audioStarted) "audio recorder start" else "audio recorder start failed")
|
||||||
}
|
}
|
||||||
|
captureRestartInVoiceCall = false
|
||||||
checkMediaPermission()
|
checkMediaPermission()
|
||||||
_isStart = true
|
_isStart = true
|
||||||
FFI.setFrameRawEnable("video",true)
|
FFI.setFrameRawEnable("video",true)
|
||||||
@@ -447,9 +611,24 @@ class MainService : Service() {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun releaseFailedVideoCapture() {
|
||||||
|
imageReader?.close()
|
||||||
|
imageReader = null
|
||||||
|
videoEncoder?.let {
|
||||||
|
it.signalEndOfInputStream()
|
||||||
|
it.stop()
|
||||||
|
it.release()
|
||||||
|
}
|
||||||
|
videoEncoder = null
|
||||||
|
surface?.release()
|
||||||
|
surface = null
|
||||||
|
}
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun stopCapture() {
|
fun stopCapture() {
|
||||||
Log.d(logTag, "Stop Capture")
|
Log.d(logTag, "Stop Capture")
|
||||||
|
captureRestartPending = false
|
||||||
|
captureRestartInVoiceCall = false
|
||||||
FFI.setFrameRawEnable("video",false)
|
FFI.setFrameRawEnable("video",false)
|
||||||
_isStart = false
|
_isStart = false
|
||||||
MainActivity.rdClipboardManager?.setCaptureStarted(_isStart)
|
MainActivity.rdClipboardManager?.setCaptureStarted(_isStart)
|
||||||
@@ -480,8 +659,11 @@ class MainService : Service() {
|
|||||||
surface?.release()
|
surface?.release()
|
||||||
|
|
||||||
// release audio
|
// release audio
|
||||||
_isAudioStart = false
|
stopMicrophoneCapture {
|
||||||
audioRecordHandle.tryReleaseAudio()
|
_isAudioStart = false
|
||||||
|
audioRecordHandle.tryReleaseAudio()
|
||||||
|
true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun destroy() {
|
fun destroy() {
|
||||||
@@ -496,7 +678,9 @@ class MainService : Service() {
|
|||||||
virtualDisplay = null
|
virtualDisplay = null
|
||||||
}
|
}
|
||||||
|
|
||||||
mediaProjection = null
|
releaseMediaProjection()
|
||||||
|
mediaProjectionForegroundService = false
|
||||||
|
microphoneForegroundService = false
|
||||||
checkMediaPermission()
|
checkMediaPermission()
|
||||||
stopForeground(true)
|
stopForeground(true)
|
||||||
stopService(Intent(this, FloatingWindowService::class.java))
|
stopService(Intent(this, FloatingWindowService::class.java))
|
||||||
@@ -519,49 +703,70 @@ class MainService : Service() {
|
|||||||
return isReady
|
return isReady
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startRawVideoRecorder(mp: MediaProjection) {
|
private fun startRawVideoRecorder(mp: MediaProjection): Boolean {
|
||||||
Log.d(logTag, "startRawVideoRecorder,screen info:$SCREEN_INFO")
|
Log.d(logTag, "startRawVideoRecorder,screen info:$SCREEN_INFO")
|
||||||
if (surface == null) {
|
val captureSurface = surface
|
||||||
|
if (captureSurface == null) {
|
||||||
Log.d(logTag, "startRawVideoRecorder failed,surface is null")
|
Log.d(logTag, "startRawVideoRecorder failed,surface is null")
|
||||||
return
|
return false
|
||||||
}
|
}
|
||||||
createOrSetVirtualDisplay(mp, surface!!)
|
return createOrSetVirtualDisplay(mp, captureSurface)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startVP9VideoRecorder(mp: MediaProjection) {
|
private fun startVP9VideoRecorder(mp: MediaProjection): Boolean {
|
||||||
createMediaCodec()
|
createMediaCodec()
|
||||||
videoEncoder?.let {
|
val encoder = videoEncoder ?: return false
|
||||||
surface = it.createInputSurface()
|
val inputSurface = encoder.createInputSurface()
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
surface = inputSurface
|
||||||
surface!!.setFrameRate(1F, FRAME_RATE_COMPATIBILITY_DEFAULT)
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
}
|
inputSurface.setFrameRate(1F, FRAME_RATE_COMPATIBILITY_DEFAULT)
|
||||||
it.setCallback(cb)
|
|
||||||
it.start()
|
|
||||||
createOrSetVirtualDisplay(mp, surface!!)
|
|
||||||
}
|
}
|
||||||
|
encoder.setCallback(cb)
|
||||||
|
encoder.start()
|
||||||
|
return createOrSetVirtualDisplay(mp, inputSurface)
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://github.com/bk138/droidVNC-NG/blob/b79af62db5a1c08ed94e6a91464859ffed6f4e97/app/src/main/java/net/christianbeier/droidvnc_ng/MediaProjectionService.java#L250
|
// https://github.com/bk138/droidVNC-NG/blob/b79af62db5a1c08ed94e6a91464859ffed6f4e97/app/src/main/java/net/christianbeier/droidvnc_ng/MediaProjectionService.java#L250
|
||||||
// Reuse virtualDisplay if it exists, to avoid media projection confirmation dialog every connection.
|
// Reuse virtualDisplay if it exists, to avoid media projection confirmation dialog every connection.
|
||||||
private fun createOrSetVirtualDisplay(mp: MediaProjection, s: Surface) {
|
private fun createOrSetVirtualDisplay(mp: MediaProjection, s: Surface): Boolean {
|
||||||
try {
|
return try {
|
||||||
virtualDisplay?.let {
|
val existingDisplay = virtualDisplay
|
||||||
it.resize(SCREEN_INFO.width, SCREEN_INFO.height, SCREEN_INFO.dpi)
|
if (existingDisplay != null) {
|
||||||
it.setSurface(s)
|
existingDisplay.resize(SCREEN_INFO.width, SCREEN_INFO.height, SCREEN_INFO.dpi)
|
||||||
} ?: let {
|
existingDisplay.setSurface(s)
|
||||||
virtualDisplay = mp.createVirtualDisplay(
|
true
|
||||||
|
} else {
|
||||||
|
val display = mp.createVirtualDisplay(
|
||||||
"RustDeskVD",
|
"RustDeskVD",
|
||||||
SCREEN_INFO.width, SCREEN_INFO.height, SCREEN_INFO.dpi, VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
|
SCREEN_INFO.width, SCREEN_INFO.height, SCREEN_INFO.dpi, VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
|
||||||
s, null, null
|
s, null, null
|
||||||
)
|
)
|
||||||
|
if (display == null) {
|
||||||
|
Log.e(logTag, "createOrSetVirtualDisplay failed")
|
||||||
|
handleVirtualDisplayFailure()
|
||||||
|
} else {
|
||||||
|
virtualDisplay = display
|
||||||
|
true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e: SecurityException) {
|
} catch (e: SecurityException) {
|
||||||
Log.w(logTag, "createOrSetVirtualDisplay: got SecurityException, re-requesting confirmation");
|
Log.w(logTag, "createOrSetVirtualDisplay: got SecurityException", e)
|
||||||
// This initiates a prompt dialog for the user to confirm screen projection.
|
handleVirtualDisplayFailure()
|
||||||
requestMediaProjection()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun handleVirtualDisplayFailure(): Boolean {
|
||||||
|
captureRestartPending = true
|
||||||
|
virtualDisplay?.release()
|
||||||
|
virtualDisplay = null
|
||||||
|
releaseMediaProjection()
|
||||||
|
setMediaProjectionForegroundService(false)
|
||||||
|
_isReady = false
|
||||||
|
checkMediaPermission()
|
||||||
|
requestMediaProjection(true)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
private val cb: MediaCodec.Callback = object : MediaCodec.Callback() {
|
private val cb: MediaCodec.Callback = object : MediaCodec.Callback() {
|
||||||
override fun onInputBufferAvailable(codec: MediaCodec, index: Int) {}
|
override fun onInputBufferAvailable(codec: MediaCodec, index: Int) {}
|
||||||
override fun onOutputFormatChanged(codec: MediaCodec, format: MediaFormat) {}
|
override fun onOutputFormatChanged(codec: MediaCodec, format: MediaFormat) {}
|
||||||
@@ -652,7 +857,63 @@ class MainService : Service() {
|
|||||||
.setColor(ContextCompat.getColor(this, R.color.primary))
|
.setColor(ContextCompat.getColor(this, R.color.primary))
|
||||||
.setWhen(System.currentTimeMillis())
|
.setWhen(System.currentTimeMillis())
|
||||||
.build()
|
.build()
|
||||||
startForeground(DEFAULT_NOTIFY_ID, notification)
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
startForeground(DEFAULT_NOTIFY_ID, notification, foregroundServiceType())
|
||||||
|
} else {
|
||||||
|
startForeground(DEFAULT_NOTIFY_ID, notification)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.Q)
|
||||||
|
private fun foregroundServiceType(): Int {
|
||||||
|
var serviceType = ServiceInfo.FOREGROUND_SERVICE_TYPE_NONE
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||||
|
// Keep a valid FGS type while the unattended host is idle and no capture type is active.
|
||||||
|
serviceType = serviceType or ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE
|
||||||
|
}
|
||||||
|
if (mediaProjectionForegroundService) {
|
||||||
|
serviceType = serviceType or ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION
|
||||||
|
}
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && microphoneForegroundService) {
|
||||||
|
serviceType = serviceType or ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE
|
||||||
|
}
|
||||||
|
return serviceType
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setMediaProjectionForegroundService(enabled: Boolean): Boolean {
|
||||||
|
return updateForegroundServiceTypes(enabled, microphoneForegroundService)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setMicrophoneForegroundService(enabled: Boolean): Boolean {
|
||||||
|
return updateForegroundServiceTypes(mediaProjectionForegroundService, enabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateForegroundServiceTypes(
|
||||||
|
mediaProjectionEnabled: Boolean,
|
||||||
|
microphoneEnabled: Boolean,
|
||||||
|
): Boolean {
|
||||||
|
if (mediaProjectionForegroundService == mediaProjectionEnabled &&
|
||||||
|
microphoneForegroundService == microphoneEnabled) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
val previousMediaProjection = mediaProjectionForegroundService
|
||||||
|
val previousMicrophone = microphoneForegroundService
|
||||||
|
mediaProjectionForegroundService = mediaProjectionEnabled
|
||||||
|
microphoneForegroundService = microphoneEnabled
|
||||||
|
return try {
|
||||||
|
createForegroundNotification()
|
||||||
|
true
|
||||||
|
} catch (error: SecurityException) {
|
||||||
|
mediaProjectionForegroundService = previousMediaProjection
|
||||||
|
microphoneForegroundService = previousMicrophone
|
||||||
|
Log.e(logTag, "Failed to update foreground service types", error)
|
||||||
|
false
|
||||||
|
} catch (error: IllegalStateException) {
|
||||||
|
mediaProjectionForegroundService = previousMediaProjection
|
||||||
|
microphoneForegroundService = previousMicrophone
|
||||||
|
Log.e(logTag, "Failed to update foreground service types", error)
|
||||||
|
false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loginRequestNotification(
|
private fun loginRequestNotification(
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import android.content.Intent
|
|||||||
import android.media.projection.MediaProjectionManager
|
import android.media.projection.MediaProjectionManager
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.os.ResultReceiver
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
|
||||||
class PermissionRequestTransparentActivity: Activity() {
|
class PermissionRequestTransparentActivity: Activity() {
|
||||||
@@ -31,7 +32,13 @@ class PermissionRequestTransparentActivity: Activity() {
|
|||||||
if (resultCode == RESULT_OK && data != null) {
|
if (resultCode == RESULT_OK && data != null) {
|
||||||
launchService(data)
|
launchService(data)
|
||||||
} else {
|
} else {
|
||||||
setResult(RES_FAILED)
|
val resultReceiver =
|
||||||
|
intent.getParcelableExtra<ResultReceiver>(EXT_MEDIA_PROJECTION_RESULT_RECEIVER)
|
||||||
|
if (resultReceiver != null) {
|
||||||
|
resultReceiver.send(RES_FAILED, null)
|
||||||
|
} else {
|
||||||
|
setResult(RES_FAILED)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,4 +58,4 @@ class PermissionRequestTransparentActivity: Activity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,11 +33,16 @@ const val ACT_INIT_MEDIA_PROJECTION_AND_SERVICE = "INIT_MEDIA_PROJECTION_AND_SER
|
|||||||
const val ACT_LOGIN_REQ_NOTIFY = "LOGIN_REQ_NOTIFY"
|
const val ACT_LOGIN_REQ_NOTIFY = "LOGIN_REQ_NOTIFY"
|
||||||
const val EXT_INIT_FROM_BOOT = "EXT_INIT_FROM_BOOT"
|
const val EXT_INIT_FROM_BOOT = "EXT_INIT_FROM_BOOT"
|
||||||
const val EXT_MEDIA_PROJECTION_RES_INTENT = "MEDIA_PROJECTION_RES_INTENT"
|
const val EXT_MEDIA_PROJECTION_RES_INTENT = "MEDIA_PROJECTION_RES_INTENT"
|
||||||
|
const val EXT_MEDIA_PROJECTION_RESULT_RECEIVER = "MEDIA_PROJECTION_RESULT_RECEIVER"
|
||||||
const val EXT_LOGIN_REQ_NOTIFY = "LOGIN_REQ_NOTIFY"
|
const val EXT_LOGIN_REQ_NOTIFY = "LOGIN_REQ_NOTIFY"
|
||||||
|
|
||||||
// Activity requestCode
|
// Activity requestCode
|
||||||
const val REQ_INVOKE_PERMISSION_ACTIVITY_MEDIA_PROJECTION = 101
|
const val REQ_INVOKE_PERMISSION_ACTIVITY_MEDIA_PROJECTION = 101
|
||||||
const val REQ_REQUEST_MEDIA_PROJECTION = 201
|
const val REQ_REQUEST_MEDIA_PROJECTION = 201
|
||||||
|
const val REQ_EXPORT_FILE = 301
|
||||||
|
const val REQ_IMPORT_FILES = 302
|
||||||
|
const val REQ_IMPORT_DIRECTORY = 303
|
||||||
|
const val REQ_EXPORT_FILES = 304
|
||||||
|
|
||||||
// Activity responseCode
|
// Activity responseCode
|
||||||
const val RES_FAILED = -100
|
const val RES_FAILED = -100
|
||||||
@@ -47,6 +52,12 @@ const val START_ACTION = "start_action"
|
|||||||
const val GET_START_ON_BOOT_OPT = "get_start_on_boot_opt"
|
const val GET_START_ON_BOOT_OPT = "get_start_on_boot_opt"
|
||||||
const val SET_START_ON_BOOT_OPT = "set_start_on_boot_opt"
|
const val SET_START_ON_BOOT_OPT = "set_start_on_boot_opt"
|
||||||
const val SYNC_APP_DIR_CONFIG_PATH = "sync_app_dir"
|
const val SYNC_APP_DIR_CONFIG_PATH = "sync_app_dir"
|
||||||
|
const val PICK_IMPORT_FILES = "pick_import_files"
|
||||||
|
const val IMPORT_FILE = "import_file"
|
||||||
|
const val EXPORT_FILE = "export_file"
|
||||||
|
const val PICK_IMPORT_DIRECTORY = "pick_import_directory"
|
||||||
|
const val IMPORT_DIRECTORY = "import_directory"
|
||||||
|
const val EXPORT_FILES = "export_files"
|
||||||
const val GET_VALUE = "get_value"
|
const val GET_VALUE = "get_value"
|
||||||
|
|
||||||
const val KEY_IS_SUPPORT_VOICE_CALL = "KEY_IS_SUPPORT_VOICE_CALL"
|
const val KEY_IS_SUPPORT_VOICE_CALL = "KEY_IS_SUPPORT_VOICE_CALL"
|
||||||
@@ -154,4 +165,4 @@ fun getScreenSize(windowManager: WindowManager) : Pair<Int, Int>{
|
|||||||
fun translate(input: String): String {
|
fun translate(input: String): String {
|
||||||
Log.d("common", "translate:$LOCAL_NAME")
|
Log.d("common", "translate:$LOCAL_NAME")
|
||||||
return FFI.translateLocale(LOCAL_NAME, input)
|
return FFI.translateLocale(LOCAL_NAME, input)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ object FFI {
|
|||||||
external fun init(ctx: Context)
|
external fun init(ctx: Context)
|
||||||
external fun onAppStart(ctx: Context)
|
external fun onAppStart(ctx: Context)
|
||||||
external fun setClipboardManager(clipboardManager: RdClipboardManager)
|
external fun setClipboardManager(clipboardManager: RdClipboardManager)
|
||||||
external fun startServer(app_dir: String, custom_client_config: String)
|
external fun startServer(app_dir: String, home_dir: String, custom_client_config: String)
|
||||||
external fun startService()
|
external fun startService()
|
||||||
external fun onVideoFrameUpdate(buf: ByteBuffer)
|
external fun onVideoFrameUpdate(buf: ByteBuffer)
|
||||||
external fun onAudioFrameUpdate(buf: ByteBuffer)
|
external fun onAudioFrameUpdate(buf: ByteBuffer)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">RustDesk</string>
|
<string name="app_name">RustDesk</string>
|
||||||
<string name="accessibility_service_description">Allow other devices to control your phone using virtual touch, when RustDesk screen sharing is established</string>
|
<string name="accessibility_service_description">Allow other devices to control your phone using virtual touch, when RustDesk screen sharing is established</string>
|
||||||
|
<string name="foreground_service_special_use_subtype">Keeps the RustDesk remote desktop host available for authorized unattended connections and foreground notifications without starting screen capture before user approval.</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1,3 +1,29 @@
|
|||||||
|
def legacyPluginNamespaces = [
|
||||||
|
external_path: 'com.pinciat.external_path',
|
||||||
|
flutter_keyboard_visibility: 'com.jrai.flutter_keyboard_visibility',
|
||||||
|
qr_code_scanner: 'net.touchcapture.qr.flutterqr',
|
||||||
|
sqflite: 'com.tekartik.sqflite',
|
||||||
|
uni_links: 'name.avioli.unilinks',
|
||||||
|
]
|
||||||
|
|
||||||
|
def java8JvmTarget = JavaVersion.VERSION_1_8.toString()
|
||||||
|
def java8KotlinJvmTargets = [
|
||||||
|
app: java8JvmTarget,
|
||||||
|
external_path: java8JvmTarget,
|
||||||
|
qr_code_scanner: java8JvmTarget,
|
||||||
|
]
|
||||||
|
|
||||||
|
def configureKotlinJvmTarget = { Project project, String kotlinJvmTarget ->
|
||||||
|
project.plugins.withId('kotlin-android') {
|
||||||
|
project.tasks.configureEach { task ->
|
||||||
|
if (!task.hasProperty('kotlinOptions')) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
task.kotlinOptions.jvmTarget = kotlinJvmTarget
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
allprojects {
|
allprojects {
|
||||||
repositories {
|
repositories {
|
||||||
google()
|
google()
|
||||||
@@ -9,6 +35,16 @@ allprojects {
|
|||||||
rootProject.buildDir = '../build'
|
rootProject.buildDir = '../build'
|
||||||
subprojects {
|
subprojects {
|
||||||
project.buildDir = "${rootProject.buildDir}/${project.name}"
|
project.buildDir = "${rootProject.buildDir}/${project.name}"
|
||||||
|
def legacyNamespace = legacyPluginNamespaces[project.name]
|
||||||
|
if (legacyNamespace != null) {
|
||||||
|
project.plugins.withId('com.android.library') {
|
||||||
|
project.android.namespace = legacyNamespace
|
||||||
|
}
|
||||||
|
}
|
||||||
|
def kotlinJvmTarget = java8KotlinJvmTargets[project.name]
|
||||||
|
if (kotlinJvmTarget != null) {
|
||||||
|
configureKotlinJvmTarget(project, kotlinJvmTarget)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
subprojects {
|
subprojects {
|
||||||
project.evaluationDependsOn(':app')
|
project.evaluationDependsOn(':app')
|
||||||
|
|||||||
@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
|
|||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.4-all.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ pluginManagement {
|
|||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
||||||
id "com.android.application" version "7.3.1" apply false
|
id "com.android.application" version "8.10.1" apply false
|
||||||
id "org.jetbrains.kotlin.android" version "2.1.21" apply false
|
id "org.jetbrains.kotlin.android" version "2.1.21" apply false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -84,8 +84,6 @@ const double _kPositionEpsilon = 1e-6;
|
|||||||
bool get isMainDesktopWindow =>
|
bool get isMainDesktopWindow =>
|
||||||
desktopType == DesktopType.main || desktopType == DesktopType.cm;
|
desktopType == DesktopType.main || desktopType == DesktopType.cm;
|
||||||
|
|
||||||
String get screenInfo => screenInfo_;
|
|
||||||
|
|
||||||
/// Check if the app is running with single view mode.
|
/// Check if the app is running with single view mode.
|
||||||
bool isSingleViewApp() {
|
bool isSingleViewApp() {
|
||||||
return desktopType == DesktopType.cm;
|
return desktopType == DesktopType.cm;
|
||||||
@@ -1521,13 +1519,6 @@ class AndroidPermissionManager {
|
|||||||
static Timer? _timer;
|
static Timer? _timer;
|
||||||
static var _current = "";
|
static var _current = "";
|
||||||
|
|
||||||
static bool isWaitingFile() {
|
|
||||||
if (_completer != null) {
|
|
||||||
return !_completer!.isCompleted && _current == kManageExternalStorage;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future<bool> check(String type) {
|
static Future<bool> check(String type) {
|
||||||
if (isDesktop || isWeb) {
|
if (isDesktop || isWeb) {
|
||||||
return Future.value(true);
|
return Future.value(true);
|
||||||
@@ -1642,7 +1633,8 @@ String bool2option(String option, bool b) {
|
|||||||
String res;
|
String res;
|
||||||
if (option.startsWith('enable-') &&
|
if (option.startsWith('enable-') &&
|
||||||
option != kOptionEnableUdpPunch &&
|
option != kOptionEnableUdpPunch &&
|
||||||
option != kOptionEnableIpv6Punch) {
|
option != kOptionEnableIpv6Punch &&
|
||||||
|
option != kOptionEnableWebrtc) {
|
||||||
res = b ? defaultOptionYes : 'N';
|
res = b ? defaultOptionYes : 'N';
|
||||||
} else if (option.startsWith('allow-') ||
|
} else if (option.startsWith('allow-') ||
|
||||||
option == kOptionStopService ||
|
option == kOptionStopService ||
|
||||||
@@ -2636,13 +2628,6 @@ connect(BuildContext context, String id,
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (isFileTransfer) {
|
if (isFileTransfer) {
|
||||||
if (isAndroid) {
|
|
||||||
if (!await AndroidPermissionManager.check(kManageExternalStorage)) {
|
|
||||||
if (!await AndroidPermissionManager.request(kManageExternalStorage)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (isWeb) {
|
if (isWeb) {
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
|
|||||||
@@ -936,26 +936,19 @@ void enterPasswordDialog(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void enterUserLoginDialog(
|
void enterUserLoginDialog(SessionID sessionId,
|
||||||
SessionID sessionId,
|
OverlayDialogManager dialogManager, String osAccountDescTip) async {
|
||||||
OverlayDialogManager dialogManager,
|
|
||||||
String osAccountDescTip,
|
|
||||||
bool canRememberAccount) async {
|
|
||||||
await _connectDialog(
|
await _connectDialog(
|
||||||
sessionId,
|
sessionId,
|
||||||
dialogManager,
|
dialogManager,
|
||||||
osUsernameController: TextEditingController(),
|
osUsernameController: TextEditingController(),
|
||||||
osPasswordController: TextEditingController(),
|
osPasswordController: TextEditingController(),
|
||||||
osAccountDescTip: osAccountDescTip,
|
osAccountDescTip: osAccountDescTip,
|
||||||
canRememberAccount: canRememberAccount,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void enterUserLoginAndPasswordDialog(
|
void enterUserLoginAndPasswordDialog(SessionID sessionId,
|
||||||
SessionID sessionId,
|
OverlayDialogManager dialogManager, String osAccountDescTip) async {
|
||||||
OverlayDialogManager dialogManager,
|
|
||||||
String osAccountDescTip,
|
|
||||||
bool canRememberAccount) async {
|
|
||||||
await _connectDialog(
|
await _connectDialog(
|
||||||
sessionId,
|
sessionId,
|
||||||
dialogManager,
|
dialogManager,
|
||||||
@@ -963,7 +956,6 @@ void enterUserLoginAndPasswordDialog(
|
|||||||
osPasswordController: TextEditingController(),
|
osPasswordController: TextEditingController(),
|
||||||
passwordController: TextEditingController(),
|
passwordController: TextEditingController(),
|
||||||
osAccountDescTip: osAccountDescTip,
|
osAccountDescTip: osAccountDescTip,
|
||||||
canRememberAccount: canRememberAccount,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -974,7 +966,6 @@ _connectDialog(
|
|||||||
TextEditingController? osPasswordController,
|
TextEditingController? osPasswordController,
|
||||||
TextEditingController? passwordController,
|
TextEditingController? passwordController,
|
||||||
String? osAccountDescTip,
|
String? osAccountDescTip,
|
||||||
bool canRememberAccount = true,
|
|
||||||
}) async {
|
}) async {
|
||||||
final errUsername = ''.obs;
|
final errUsername = ''.obs;
|
||||||
var rememberPassword = false;
|
var rememberPassword = false;
|
||||||
@@ -982,11 +973,6 @@ _connectDialog(
|
|||||||
rememberPassword =
|
rememberPassword =
|
||||||
await bind.sessionGetRemember(sessionId: sessionId) ?? false;
|
await bind.sessionGetRemember(sessionId: sessionId) ?? false;
|
||||||
}
|
}
|
||||||
var rememberAccount = false;
|
|
||||||
if (canRememberAccount && osUsernameController != null) {
|
|
||||||
rememberAccount =
|
|
||||||
await bind.sessionGetRemember(sessionId: sessionId) ?? false;
|
|
||||||
}
|
|
||||||
if (osUsernameController != null) {
|
if (osUsernameController != null) {
|
||||||
osUsernameController.addListener(() {
|
osUsernameController.addListener(() {
|
||||||
if (errUsername.value.isNotEmpty) {
|
if (errUsername.value.isNotEmpty) {
|
||||||
@@ -1014,12 +1000,6 @@ _connectDialog(
|
|||||||
final osPassword = osPasswordController?.text.trim() ?? '';
|
final osPassword = osPasswordController?.text.trim() ?? '';
|
||||||
final password = passwordController?.text.trim() ?? '';
|
final password = passwordController?.text.trim() ?? '';
|
||||||
if (passwordController != null && password.isEmpty) return;
|
if (passwordController != null && password.isEmpty) return;
|
||||||
if (rememberAccount) {
|
|
||||||
bind.sessionPeerOption(
|
|
||||||
sessionId: sessionId, name: 'os-username', value: osUsername);
|
|
||||||
bind.sessionPeerOption(
|
|
||||||
sessionId: sessionId, name: 'os-password', value: osPassword);
|
|
||||||
}
|
|
||||||
gFFI.login(
|
gFFI.login(
|
||||||
osUsername,
|
osUsername,
|
||||||
osPassword,
|
osPassword,
|
||||||
@@ -1096,16 +1076,6 @@ _connectDialog(
|
|||||||
controller: osPasswordController,
|
controller: osPasswordController,
|
||||||
autoFocus: false,
|
autoFocus: false,
|
||||||
),
|
),
|
||||||
if (canRememberAccount)
|
|
||||||
rememberWidget(
|
|
||||||
translate('remember_account_tip'),
|
|
||||||
rememberAccount,
|
|
||||||
(v) {
|
|
||||||
if (v != null) {
|
|
||||||
setState(() => rememberAccount = v);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1542,91 +1512,6 @@ showSetOSPassword(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
showSetOSAccount(
|
|
||||||
SessionID sessionId,
|
|
||||||
OverlayDialogManager dialogManager,
|
|
||||||
) async {
|
|
||||||
final usernameController = TextEditingController();
|
|
||||||
final passwdController = TextEditingController();
|
|
||||||
var username =
|
|
||||||
await bind.sessionGetOption(sessionId: sessionId, arg: 'os-username') ??
|
|
||||||
'';
|
|
||||||
var password =
|
|
||||||
await bind.sessionGetOption(sessionId: sessionId, arg: 'os-password') ??
|
|
||||||
'';
|
|
||||||
usernameController.text = username;
|
|
||||||
passwdController.text = password;
|
|
||||||
dialogManager.show((setState, close, context) {
|
|
||||||
submit() {
|
|
||||||
final username = usernameController.text.trim();
|
|
||||||
final password = usernameController.text.trim();
|
|
||||||
bind.sessionPeerOption(
|
|
||||||
sessionId: sessionId, name: 'os-username', value: username);
|
|
||||||
bind.sessionPeerOption(
|
|
||||||
sessionId: sessionId, name: 'os-password', value: password);
|
|
||||||
close();
|
|
||||||
}
|
|
||||||
|
|
||||||
descWidget(String text) {
|
|
||||||
return Column(
|
|
||||||
children: [
|
|
||||||
Align(
|
|
||||||
alignment: Alignment.centerLeft,
|
|
||||||
child: Text(
|
|
||||||
text,
|
|
||||||
maxLines: 3,
|
|
||||||
softWrap: true,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
style: TextStyle(fontSize: 16),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Container(
|
|
||||||
height: 8,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return CustomAlertDialog(
|
|
||||||
title: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
Icon(Icons.password_rounded, color: MyTheme.accent),
|
|
||||||
Text(translate('OS Account')).paddingOnly(left: 10),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
content: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
descWidget(translate("os_account_desk_tip")),
|
|
||||||
DialogTextField(
|
|
||||||
title: translate(DialogTextField.kUsernameTitle),
|
|
||||||
controller: usernameController,
|
|
||||||
prefixIcon: DialogTextField.kUsernameIcon,
|
|
||||||
errorText: null,
|
|
||||||
),
|
|
||||||
PasswordWidget(controller: passwdController),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
actions: [
|
|
||||||
dialogButton(
|
|
||||||
"Cancel",
|
|
||||||
icon: Icon(Icons.close_rounded),
|
|
||||||
onPressed: close,
|
|
||||||
isOutline: true,
|
|
||||||
),
|
|
||||||
dialogButton(
|
|
||||||
"OK",
|
|
||||||
icon: Icon(Icons.done_rounded),
|
|
||||||
onPressed: submit,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
onSubmit: submit,
|
|
||||||
onCancel: close,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget buildNoteTextField({
|
Widget buildNoteTextField({
|
||||||
required TextEditingController controller,
|
required TextEditingController controller,
|
||||||
required VoidCallback onEscape,
|
required VoidCallback onEscape,
|
||||||
@@ -2014,26 +1899,110 @@ customImageQualityDialog(SessionID sessionId, String id, FFI ffi) async {
|
|||||||
msgBoxCommon(ffi.dialogManager, 'Custom Image Quality', content, [btnClose]);
|
msgBoxCommon(ffi.dialogManager, 'Custom Image Quality', content, [btnClose]);
|
||||||
}
|
}
|
||||||
|
|
||||||
trackpadSpeedDialog(SessionID sessionId, FFI ffi) async {
|
int? _validateTrackpadSpeed(String text) {
|
||||||
int initSpeed = ffi.inputModel.trackpadSpeed;
|
final speed = int.tryParse(text);
|
||||||
|
if (speed == null || speed < kMinTrackpadSpeed || speed > kMaxTrackpadSpeed) {
|
||||||
|
BotToast.showText(
|
||||||
|
text:
|
||||||
|
'${translate('Invalid format')}: $kMinTrackpadSpeed-$kMaxTrackpadSpeed',
|
||||||
|
contentColor: Colors.red,
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return speed;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _saveTrackpadSpeed({
|
||||||
|
required SessionID sessionId,
|
||||||
|
required FFI ffi,
|
||||||
|
required int initSpeed,
|
||||||
|
required int speed,
|
||||||
|
}) async {
|
||||||
|
if (speed == initSpeed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await bind.sessionSetTrackpadSpeed(sessionId: sessionId, value: speed);
|
||||||
|
await ffi.inputModel.updateTrackpadSpeed();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showTrackpadSpeedSaveError(Object error, StackTrace stackTrace) {
|
||||||
|
debugPrint('Failed to save trackpad speed: $error');
|
||||||
|
debugPrintStack(stackTrace: stackTrace);
|
||||||
|
BotToast.showText(
|
||||||
|
text: translate('Failed'),
|
||||||
|
contentColor: Colors.red,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Widget> _trackpadSpeedDialogActions({
|
||||||
|
required bool isSubmitting,
|
||||||
|
required VoidCallback close,
|
||||||
|
required VoidCallback submit,
|
||||||
|
}) {
|
||||||
|
return [
|
||||||
|
dialogButton(
|
||||||
|
'Cancel',
|
||||||
|
icon: Icon(Icons.close_rounded),
|
||||||
|
onPressed: isSubmitting ? null : close,
|
||||||
|
isOutline: true,
|
||||||
|
),
|
||||||
|
dialogButton(
|
||||||
|
'OK',
|
||||||
|
icon: Icon(Icons.done_rounded),
|
||||||
|
onPressed: isSubmitting ? null : submit,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
void trackpadSpeedDialog(SessionID sessionId, FFI ffi) {
|
||||||
|
final initSpeed = ffi.inputModel.trackpadSpeed;
|
||||||
final curSpeed = SimpleWrapper(initSpeed);
|
final curSpeed = SimpleWrapper(initSpeed);
|
||||||
final btnClose = dialogButton('Close', onPressed: () async {
|
var speedText = initSpeed.toString();
|
||||||
if (curSpeed.value <= kMaxTrackpadSpeed &&
|
var isSubmitting = false;
|
||||||
curSpeed.value >= kMinTrackpadSpeed &&
|
ffi.dialogManager.show((setState, close, context) {
|
||||||
curSpeed.value != initSpeed) {
|
Future<void> submit([String? submittedText]) async {
|
||||||
await bind.sessionSetTrackpadSpeed(
|
if (isSubmitting) {
|
||||||
sessionId: sessionId, value: curSpeed.value);
|
return;
|
||||||
await ffi.inputModel.updateTrackpadSpeed();
|
}
|
||||||
|
speedText = submittedText ?? speedText;
|
||||||
|
final speed = _validateTrackpadSpeed(speedText);
|
||||||
|
if (speed == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setState(() => isSubmitting = true);
|
||||||
|
try {
|
||||||
|
await _saveTrackpadSpeed(
|
||||||
|
sessionId: sessionId,
|
||||||
|
ffi: ffi,
|
||||||
|
initSpeed: initSpeed,
|
||||||
|
speed: speed,
|
||||||
|
);
|
||||||
|
close();
|
||||||
|
} catch (error, stackTrace) {
|
||||||
|
_showTrackpadSpeedSaveError(error, stackTrace);
|
||||||
|
setState(() => isSubmitting = false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
ffi.dialogManager.dismissAll();
|
|
||||||
});
|
return CustomAlertDialog(
|
||||||
msgBoxCommon(
|
title: Text(
|
||||||
ffi.dialogManager,
|
translate('Trackpad speed'),
|
||||||
'Trackpad speed',
|
style: TextStyle(fontSize: 21),
|
||||||
TrackpadSpeedWidget(
|
|
||||||
value: curSpeed,
|
|
||||||
),
|
),
|
||||||
[btnClose]);
|
content: TrackpadSpeedWidget(
|
||||||
|
value: curSpeed,
|
||||||
|
onTextChanged: (text) => speedText = text,
|
||||||
|
onTextSubmitted: submit,
|
||||||
|
),
|
||||||
|
actions: _trackpadSpeedDialogActions(
|
||||||
|
isSubmitting: isSubmitting,
|
||||||
|
close: close,
|
||||||
|
submit: submit,
|
||||||
|
),
|
||||||
|
onSubmit: isSubmitting ? null : submit,
|
||||||
|
onCancel: isSubmitting ? null : close,
|
||||||
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void deleteConfirmDialog(Function onSubmit, String title) async {
|
void deleteConfirmDialog(Function onSubmit, String title) async {
|
||||||
|
|||||||
@@ -606,6 +606,9 @@ class QualityMonitor extends StatelessWidget {
|
|||||||
_row(
|
_row(
|
||||||
"Codec", qualityMonitorModel.data.codecFormat ?? '-'),
|
"Codec", qualityMonitorModel.data.codecFormat ?? '-'),
|
||||||
_row("Chroma", qualityMonitorModel.data.chroma ?? '-'),
|
_row("Chroma", qualityMonitorModel.data.chroma ?? '-'),
|
||||||
|
if (qualityMonitorModel.webrtcTransport != null)
|
||||||
|
_row("Transport",
|
||||||
|
qualityMonitorModel.webrtcTransport!),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -115,6 +115,7 @@ class _RawTouchGestureDetectorRegionState
|
|||||||
InputModel get inputModel => widget.inputModel;
|
InputModel get inputModel => widget.inputModel;
|
||||||
bool get handleTouch => (isDesktop || isWebDesktop) || ffiModel.touchMode;
|
bool get handleTouch => (isDesktop || isWebDesktop) || ffiModel.touchMode;
|
||||||
SessionID get sessionId => ffi.sessionId;
|
SessionID get sessionId => ffi.sessionId;
|
||||||
|
bool get canvasLocked => isMobile && ffi.canvasModel.locked;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -471,6 +472,8 @@ class _RawTouchGestureDetectorRegionState
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (canvasLocked) return;
|
||||||
|
|
||||||
if ((isDesktop || isWebDesktop)) {
|
if ((isDesktop || isWebDesktop)) {
|
||||||
final scale = ((d.scale - _scale) * 1000).toInt();
|
final scale = ((d.scale - _scale) * 1000).toInt();
|
||||||
_scale = d.scale;
|
_scale = d.scale;
|
||||||
|
|||||||
@@ -244,17 +244,54 @@ List<(String, String)> otherDefaultSettings() {
|
|||||||
kKeyUseAllMyDisplaysForTheRemoteSession
|
kKeyUseAllMyDisplaysForTheRemoteSession
|
||||||
),
|
),
|
||||||
('Keep terminal sessions on disconnect', kOptionTerminalPersistent),
|
('Keep terminal sessions on disconnect', kOptionTerminalPersistent),
|
||||||
|
(
|
||||||
|
'Allow terminal apps to copy to clipboard',
|
||||||
|
kOptionAllowTerminalClipboardWrite
|
||||||
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
return v;
|
return v;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String getOtherDefaultSettingOption(String key) {
|
||||||
|
if (key == kOptionAllowTerminalClipboardWrite) {
|
||||||
|
return bind.mainGetLocalOption(key: key);
|
||||||
|
}
|
||||||
|
return bind.mainGetUserDefaultOption(key: key);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> setOtherDefaultSettingOption(String key, String value) {
|
||||||
|
if (key == kOptionAllowTerminalClipboardWrite) {
|
||||||
|
return bind.mainSetLocalOption(
|
||||||
|
key: key,
|
||||||
|
value: value == kTerminalClipboardWriteAllowed
|
||||||
|
? kTerminalClipboardWriteAllowed
|
||||||
|
: kTerminalClipboardWriteDenied,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return bind.mainSetUserDefaultOption(key: key, value: value);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isOtherDefaultSettingReadOnly(String key) =>
|
||||||
|
isOptionFixed(key) ||
|
||||||
|
(key == kOptionAllowTerminalClipboardWrite && bind.isDisableSettings());
|
||||||
|
|
||||||
class TrackpadSpeedWidget extends StatefulWidget {
|
class TrackpadSpeedWidget extends StatefulWidget {
|
||||||
final SimpleWrapper<int> value;
|
final SimpleWrapper<int> value;
|
||||||
// If null, no debouncer will be applied.
|
// If null, no debouncer will be applied.
|
||||||
final Function(int)? onDebouncer;
|
final Function(int)? onDebouncer;
|
||||||
|
final ValueChanged<String>? onTextChanged;
|
||||||
|
// IME actions call TextField.onSubmitted without reaching the dialog's
|
||||||
|
// raw Enter handler, so the dialog needs a separate submission callback.
|
||||||
|
final ValueChanged<String>? onTextSubmitted;
|
||||||
|
|
||||||
TrackpadSpeedWidget({Key? key, required this.value, this.onDebouncer});
|
TrackpadSpeedWidget({
|
||||||
|
Key? key,
|
||||||
|
required this.value,
|
||||||
|
this.onDebouncer,
|
||||||
|
this.onTextChanged,
|
||||||
|
this.onTextSubmitted,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
TrackpadSpeedWidgetState createState() => TrackpadSpeedWidgetState();
|
TrackpadSpeedWidgetState createState() => TrackpadSpeedWidgetState();
|
||||||
@@ -276,6 +313,34 @@ class TrackpadSpeedWidgetState extends State<TrackpadSpeedWidget> {
|
|||||||
debouncerSpeed.setValue(value);
|
debouncerSpeed.setValue(value);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
widget.onTextChanged?.call(_controller.text);
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateTextValue(String text) {
|
||||||
|
widget.onTextChanged?.call(text);
|
||||||
|
final newValue = int.tryParse(text);
|
||||||
|
if (newValue == null ||
|
||||||
|
newValue < kMinTrackpadSpeed ||
|
||||||
|
newValue > kMaxTrackpadSpeed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setState(() => value = newValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
void submitTextValue(String text) {
|
||||||
|
final onTextSubmitted = widget.onTextSubmitted;
|
||||||
|
if (onTextSubmitted != null) {
|
||||||
|
onTextSubmitted(text);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (widget.onTextChanged != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final newValue = int.tryParse(text);
|
||||||
|
if (newValue == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
updateValue(newValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -315,12 +380,8 @@ class TrackpadSpeedWidgetState extends State<TrackpadSpeedWidget> {
|
|||||||
controller: _controller,
|
controller: _controller,
|
||||||
keyboardType: TextInputType.number,
|
keyboardType: TextInputType.number,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
onSubmitted: (text) {
|
onChanged: updateTextValue,
|
||||||
int? v = int.tryParse(text);
|
onSubmitted: submitTextValue,
|
||||||
if (v != null) {
|
|
||||||
updateValue(v);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
style: const TextStyle(fontSize: 13),
|
style: const TextStyle(fontSize: 13),
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
contentPadding:
|
contentPadding:
|
||||||
|
|||||||
@@ -349,12 +349,12 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
|
|||||||
showRequestElevationDialog(sessionId, ffi.dialogManager)),
|
showRequestElevationDialog(sessionId, ffi.dialogManager)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// osAccount / osPassword
|
// osPassword
|
||||||
if (isDefaultConn && perms['keyboard'] != false) {
|
if (isDefaultConn && perms['keyboard'] != false) {
|
||||||
v.add(
|
v.add(
|
||||||
TTextMenu(
|
TTextMenu(
|
||||||
child: Row(children: [
|
child: Row(children: [
|
||||||
Text(translate(pi.isHeadless ? 'OS Account' : 'OS Password')),
|
Text(translate('OS Password')),
|
||||||
]),
|
]),
|
||||||
trailingIcon: Transform.scale(
|
trailingIcon: Transform.scale(
|
||||||
scale: (isDesktop || isWebDesktop) ? 0.8 : 1,
|
scale: (isDesktop || isWebDesktop) ? 0.8 : 1,
|
||||||
@@ -363,18 +363,12 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
|
|||||||
if (isMobile && Navigator.canPop(context)) {
|
if (isMobile && Navigator.canPop(context)) {
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
}
|
}
|
||||||
if (pi.isHeadless) {
|
handleOsPasswordEditIcon(sessionId, ffi.dialogManager);
|
||||||
showSetOSAccount(sessionId, ffi.dialogManager);
|
|
||||||
} else {
|
|
||||||
handleOsPasswordEditIcon(sessionId, ffi.dialogManager);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
icon: Icon(Icons.edit, color: isMobile ? MyTheme.accent : null),
|
icon: Icon(Icons.edit, color: isMobile ? MyTheme.accent : null),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onPressed: () => pi.isHeadless
|
onPressed: () => handleOsPasswordAction(sessionId, ffi.dialogManager),
|
||||||
? showSetOSAccount(sessionId, ffi.dialogManager)
|
|
||||||
: handleOsPasswordAction(sessionId, ffi.dialogManager),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ const kKeyMapMode = 'map';
|
|||||||
const kKeyTranslateMode = 'translate';
|
const kKeyTranslateMode = 'translate';
|
||||||
|
|
||||||
const String kPlatformAdditionsIsWayland = "is_wayland";
|
const String kPlatformAdditionsIsWayland = "is_wayland";
|
||||||
const String kPlatformAdditionsHeadless = "headless";
|
|
||||||
const String kPlatformAdditionsIsInstalled = "is_installed";
|
const String kPlatformAdditionsIsInstalled = "is_installed";
|
||||||
const String kPlatformAdditionsIddImpl = "idd_impl";
|
const String kPlatformAdditionsIddImpl = "idd_impl";
|
||||||
const String kPlatformAdditionsRustDeskVirtualDisplays =
|
const String kPlatformAdditionsRustDeskVirtualDisplays =
|
||||||
@@ -55,7 +54,6 @@ const String kAppTypeDesktopTerminal = "terminal";
|
|||||||
|
|
||||||
const String kWindowMainWindowOnTop = "main_window_on_top";
|
const String kWindowMainWindowOnTop = "main_window_on_top";
|
||||||
const String kWindowRefreshCurrentUser = "refresh_current_user";
|
const String kWindowRefreshCurrentUser = "refresh_current_user";
|
||||||
const String kWindowGetWindowInfo = "get_window_info";
|
|
||||||
const String kWindowGetScreenList = "get_screen_list";
|
const String kWindowGetScreenList = "get_screen_list";
|
||||||
// This method is not used, maybe it can be removed.
|
// This method is not used, maybe it can be removed.
|
||||||
const String kWindowDisableGrabKeyboard = "disable_grab_keyboard";
|
const String kWindowDisableGrabKeyboard = "disable_grab_keyboard";
|
||||||
@@ -88,7 +86,6 @@ const String kOptionEdgeScrollEdgeThickness = "edge-scroll-edge-thickness";
|
|||||||
const String kOptionImageQuality = "image_quality";
|
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 kOptionTextureRender = "use-texture-render";
|
||||||
const String kOptionTextureRenderHealth = "texture-render-health";
|
|
||||||
const String kOptionD3DRender = "allow-d3d-render";
|
const String kOptionD3DRender = "allow-d3d-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";
|
||||||
@@ -118,6 +115,11 @@ const String kOptionEnableAudio = "enable-audio";
|
|||||||
const String kOptionEnableCamera = "enable-camera";
|
const String kOptionEnableCamera = "enable-camera";
|
||||||
const String kOptionEnableTerminal = "enable-terminal";
|
const String kOptionEnableTerminal = "enable-terminal";
|
||||||
const String kOptionTerminalPersistent = "terminal-persistent";
|
const String kOptionTerminalPersistent = "terminal-persistent";
|
||||||
|
const String kOptionAllowTerminalClipboardWrite =
|
||||||
|
"allow-terminal-clipboard-write";
|
||||||
|
const String kTerminalClipboardWriteUnconfigured = "";
|
||||||
|
const String kTerminalClipboardWriteAllowed = "Y";
|
||||||
|
const String kTerminalClipboardWriteDenied = "N";
|
||||||
const String kOptionEnableTunnel = "enable-tunnel";
|
const String kOptionEnableTunnel = "enable-tunnel";
|
||||||
const String kOptionEnableRemoteRestart = "enable-remote-restart";
|
const String kOptionEnableRemoteRestart = "enable-remote-restart";
|
||||||
const String kOptionEnableBlockInput = "enable-block-input";
|
const String kOptionEnableBlockInput = "enable-block-input";
|
||||||
@@ -162,16 +164,20 @@ const String kOptionPeerTabVisible = "peer-tab-visible";
|
|||||||
const String kOptionPeerCardUiType = "peer-card-ui-type";
|
const String kOptionPeerCardUiType = "peer-card-ui-type";
|
||||||
const String kOptionCurrentAbName = "current-ab-name";
|
const String kOptionCurrentAbName = "current-ab-name";
|
||||||
const String kOptionEnableConfirmClosingTabs = "enable-confirm-closing-tabs";
|
const String kOptionEnableConfirmClosingTabs = "enable-confirm-closing-tabs";
|
||||||
|
const String kOptionEnablePortForwardMux = "enable-port-forward-mux";
|
||||||
const String kOptionAllowAlwaysSoftwareRender = "allow-always-software-render";
|
const String kOptionAllowAlwaysSoftwareRender = "allow-always-software-render";
|
||||||
const String kOptionEnableCheckUpdate = "enable-check-update";
|
const String kOptionEnableCheckUpdate = "enable-check-update";
|
||||||
const String kOptionAllowAutoUpdate = "allow-auto-update";
|
const String kOptionAllowAutoUpdate = "allow-auto-update";
|
||||||
const String kOptionAllowLinuxHeadless = "allow-linux-headless";
|
|
||||||
const String kOptionAllowRemoveWallpaper = "allow-remove-wallpaper";
|
const String kOptionAllowRemoveWallpaper = "allow-remove-wallpaper";
|
||||||
const String kOptionStopService = "stop-service";
|
const String kOptionStopService = "stop-service";
|
||||||
const String kOptionDirectxCapture = "enable-directx-capture";
|
const String kOptionDirectxCapture = "enable-directx-capture";
|
||||||
const String kOptionAllowRemoteCmModification = "allow-remote-cm-modification";
|
const String kOptionAllowRemoteCmModification = "allow-remote-cm-modification";
|
||||||
|
const String kOptionEnableTcpPunch = "enable-tcp-punch";
|
||||||
const String kOptionEnableUdpPunch = "enable-udp-punch";
|
const String kOptionEnableUdpPunch = "enable-udp-punch";
|
||||||
const String kOptionEnableIpv6Punch = "enable-ipv6-punch";
|
const String kOptionEnableIpv6Punch = "enable-ipv6-punch";
|
||||||
|
const String kOptionAllowSyncClipboardBetweenSessions =
|
||||||
|
"allow-sync-clipboard-between-sessions";
|
||||||
|
const String kOptionEnableWebrtc = "enable-webrtc";
|
||||||
const String kOptionEnableTrustedDevices = "enable-trusted-devices";
|
const String kOptionEnableTrustedDevices = "enable-trusted-devices";
|
||||||
const String kOptionShowVirtualMouse = "show-virtual-mouse";
|
const String kOptionShowVirtualMouse = "show-virtual-mouse";
|
||||||
const String kOptionVirtualMouseScale = "virtual-mouse-scale";
|
const String kOptionVirtualMouseScale = "virtual-mouse-scale";
|
||||||
@@ -194,6 +200,7 @@ const String kOptionHideProxySetting = "hide-proxy-settings";
|
|||||||
const String kOptionHideWebSocketSetting = "hide-websocket-settings";
|
const String kOptionHideWebSocketSetting = "hide-websocket-settings";
|
||||||
const String kOptionHideStopService = "hide-stop-service";
|
const String kOptionHideStopService = "hide-stop-service";
|
||||||
const String kOptionHideRemotePrinterSetting = "hide-remote-printer-settings";
|
const String kOptionHideRemotePrinterSetting = "hide-remote-printer-settings";
|
||||||
|
const String kOptionHideGeneralSetting = "hide-general-settings";
|
||||||
const String kOptionHideSecuritySetting = "hide-security-settings";
|
const String kOptionHideSecuritySetting = "hide-security-settings";
|
||||||
const String kOptionHideNetworkSetting = "hide-network-settings";
|
const String kOptionHideNetworkSetting = "hide-network-settings";
|
||||||
const String kOptionRemovePresetPasswordWarning =
|
const String kOptionRemovePresetPasswordWarning =
|
||||||
@@ -326,10 +333,11 @@ double kNewWindowOffset = isWindows
|
|||||||
? 30.0
|
? 30.0
|
||||||
: 50.0;
|
: 50.0;
|
||||||
|
|
||||||
|
const kDragToResizeAreaPaddingSize = 5.0;
|
||||||
EdgeInsets get kDragToResizeAreaPadding => !kUseCompatibleUiMode && isLinux
|
EdgeInsets get kDragToResizeAreaPadding => !kUseCompatibleUiMode && isLinux
|
||||||
? stateGlobal.fullscreen.isTrue || stateGlobal.isMaximized.value
|
? stateGlobal.fullscreen.isTrue || stateGlobal.isMaximized.value
|
||||||
? EdgeInsets.zero
|
? EdgeInsets.zero
|
||||||
: EdgeInsets.all(5.0)
|
: EdgeInsets.all(kDragToResizeAreaPaddingSize)
|
||||||
: EdgeInsets.zero;
|
: EdgeInsets.zero;
|
||||||
// https://en.wikipedia.org/wiki/Non-breaking_space
|
// https://en.wikipedia.org/wiki/Non-breaking_space
|
||||||
const int $nbsp = 0x00A0;
|
const int $nbsp = 0x00A0;
|
||||||
@@ -441,7 +449,6 @@ const kActionApplicationDetailsSettings =
|
|||||||
const kActionAccessibilitySettings = "android.settings.ACCESSIBILITY_SETTINGS";
|
const kActionAccessibilitySettings = "android.settings.ACCESSIBILITY_SETTINGS";
|
||||||
|
|
||||||
const kRecordAudio = "android.permission.RECORD_AUDIO";
|
const kRecordAudio = "android.permission.RECORD_AUDIO";
|
||||||
const kManageExternalStorage = "android.permission.MANAGE_EXTERNAL_STORAGE";
|
|
||||||
const kRequestIgnoreBatteryOptimizations =
|
const kRequestIgnoreBatteryOptimizations =
|
||||||
"android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS";
|
"android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS";
|
||||||
const kSystemAlertWindow = "android.permission.SYSTEM_ALERT_WINDOW";
|
const kSystemAlertWindow = "android.permission.SYSTEM_ALERT_WINDOW";
|
||||||
@@ -453,6 +460,12 @@ class AndroidChannel {
|
|||||||
static final kGetStartOnBootOpt = "get_start_on_boot_opt";
|
static final kGetStartOnBootOpt = "get_start_on_boot_opt";
|
||||||
static final kSetStartOnBootOpt = "set_start_on_boot_opt";
|
static final kSetStartOnBootOpt = "set_start_on_boot_opt";
|
||||||
static final kSyncAppDirConfigPath = "sync_app_dir";
|
static final kSyncAppDirConfigPath = "sync_app_dir";
|
||||||
|
static final kPickImportFiles = "pick_import_files";
|
||||||
|
static final kImportFile = "import_file";
|
||||||
|
static final kExportFile = "export_file";
|
||||||
|
static final kPickImportDirectory = "pick_import_directory";
|
||||||
|
static final kImportDirectory = "import_directory";
|
||||||
|
static final kExportFiles = "export_files";
|
||||||
}
|
}
|
||||||
|
|
||||||
/// flutter/packages/flutter/lib/src/services/keyboard_key.dart -> _keyLabels
|
/// flutter/packages/flutter/lib/src/services/keyboard_key.dart -> _keyLabels
|
||||||
|
|||||||
@@ -330,12 +330,14 @@ class _ConnectionPageState extends State<ConnectionPage>
|
|||||||
void onConnect(
|
void onConnect(
|
||||||
{bool isFileTransfer = false,
|
{bool isFileTransfer = false,
|
||||||
bool isViewCamera = false,
|
bool isViewCamera = false,
|
||||||
bool isTerminal = false}) {
|
bool isTerminal = false,
|
||||||
|
bool isTcpTunneling = false}) {
|
||||||
var id = _idController.id;
|
var id = _idController.id;
|
||||||
connect(context, id,
|
connect(context, id,
|
||||||
isFileTransfer: isFileTransfer,
|
isFileTransfer: isFileTransfer,
|
||||||
isViewCamera: isViewCamera,
|
isViewCamera: isViewCamera,
|
||||||
isTerminal: isTerminal);
|
isTerminal: isTerminal,
|
||||||
|
isTcpTunneling: isTcpTunneling);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// UI for the remote ID TextField.
|
/// UI for the remote ID TextField.
|
||||||
@@ -568,6 +570,14 @@ class _ConnectionPageState extends State<ConnectionPage>
|
|||||||
'${translate('Terminal')} (beta)',
|
'${translate('Terminal')} (beta)',
|
||||||
() => onConnect(isTerminal: true)
|
() => onConnect(isTerminal: true)
|
||||||
),
|
),
|
||||||
|
// `connect` routes this through the
|
||||||
|
// desktop path only; the peer card gates
|
||||||
|
// it the same way.
|
||||||
|
if (isDesktop)
|
||||||
|
(
|
||||||
|
'TCP tunneling',
|
||||||
|
() => onConnect(isTcpTunneling: true)
|
||||||
|
),
|
||||||
]
|
]
|
||||||
.map((e) => MenuEntryButton<String>(
|
.map((e) => MenuEntryButton<String>(
|
||||||
childBuilder: (TextStyle? style) =>
|
childBuilder: (TextStyle? style) =>
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ import 'package:flutter_hbb/desktop/widgets/update_progress.dart';
|
|||||||
import 'package:flutter_hbb/models/platform_model.dart';
|
import 'package:flutter_hbb/models/platform_model.dart';
|
||||||
import 'package:flutter_hbb/models/server_model.dart';
|
import 'package:flutter_hbb/models/server_model.dart';
|
||||||
import 'package:flutter_hbb/models/state_model.dart';
|
import 'package:flutter_hbb/models/state_model.dart';
|
||||||
import 'package:flutter_hbb/plugin/ui_manager.dart';
|
|
||||||
import 'package:flutter_hbb/utils/multi_window_manager.dart';
|
import 'package:flutter_hbb/utils/multi_window_manager.dart';
|
||||||
import 'package:flutter_hbb/utils/platform_channel.dart';
|
import 'package:flutter_hbb/utils/platform_channel.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
@@ -25,7 +24,6 @@ import 'package:url_launcher/url_launcher.dart';
|
|||||||
import 'package:window_manager/window_manager.dart';
|
import 'package:window_manager/window_manager.dart';
|
||||||
import 'package:window_size/window_size.dart' as window_size;
|
import 'package:window_size/window_size.dart' as window_size;
|
||||||
import '../widgets/button.dart';
|
import '../widgets/button.dart';
|
||||||
import '../widgets/texture_render_probe.dart';
|
|
||||||
|
|
||||||
class DesktopHomePage extends StatefulWidget {
|
class DesktopHomePage extends StatefulWidget {
|
||||||
const DesktopHomePage({Key? key}) : super(key: key);
|
const DesktopHomePage({Key? key}) : super(key: key);
|
||||||
@@ -61,20 +59,15 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
super.build(context);
|
super.build(context);
|
||||||
final isIncomingOnly = bind.isIncomingOnly();
|
final isIncomingOnly = bind.isIncomingOnly();
|
||||||
return Stack(
|
return _buildBlock(
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
_buildBlock(
|
buildLeftPane(context),
|
||||||
child: Row(
|
if (!isIncomingOnly) const VerticalDivider(width: 1),
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
if (!isIncomingOnly) Expanded(child: buildRightPane(context)),
|
||||||
children: [
|
|
||||||
buildLeftPane(context),
|
|
||||||
if (!isIncomingOnly) const VerticalDivider(width: 1),
|
|
||||||
if (!isIncomingOnly) Expanded(child: buildRightPane(context)),
|
|
||||||
],
|
|
||||||
)),
|
|
||||||
const Positioned(left: 0, top: 0, child: TextureRenderProbe()),
|
|
||||||
],
|
],
|
||||||
);
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildBlock({required Widget child}) {
|
Widget _buildBlock({required Widget child}) {
|
||||||
@@ -117,7 +110,6 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
buildPluginEntry(),
|
|
||||||
];
|
];
|
||||||
if (isIncomingOnly) {
|
if (isIncomingOnly) {
|
||||||
children.addAll([
|
children.addAll([
|
||||||
@@ -788,13 +780,6 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
|||||||
windowOnTop(null);
|
windowOnTop(null);
|
||||||
} else if (call.method == kWindowRefreshCurrentUser) {
|
} else if (call.method == kWindowRefreshCurrentUser) {
|
||||||
gFFI.userModel.refreshCurrentUser();
|
gFFI.userModel.refreshCurrentUser();
|
||||||
} else if (call.method == kWindowGetWindowInfo) {
|
|
||||||
final screen = (await window_size.getWindowInfo()).screen;
|
|
||||||
if (screen == null) {
|
|
||||||
return '';
|
|
||||||
} else {
|
|
||||||
return jsonEncode(screenToMap(screen));
|
|
||||||
}
|
|
||||||
} else if (call.method == kWindowGetScreenList) {
|
} else if (call.method == kWindowGetScreenList) {
|
||||||
return jsonEncode(
|
return jsonEncode(
|
||||||
(await window_size.getScreenList()).map(screenToMap).toList());
|
(await window_size.getScreenList()).map(screenToMap).toList());
|
||||||
@@ -896,21 +881,6 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
|||||||
shouldBeBlocked(_block, canBeBlocked);
|
shouldBeBlocked(_block, canBeBlocked);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget buildPluginEntry() {
|
|
||||||
final entries = PluginUiManager.instance.entries.entries;
|
|
||||||
return Offstage(
|
|
||||||
offstage: entries.isEmpty,
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
...entries.map((entry) {
|
|
||||||
return entry.value;
|
|
||||||
})
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void setPasswordDialog({VoidCallback? notEmptyCallback}) async {
|
void setPasswordDialog({VoidCallback? notEmptyCallback}) async {
|
||||||
|
|||||||
@@ -17,8 +17,6 @@ import 'package:flutter_hbb/models/platform_model.dart';
|
|||||||
import 'package:flutter_hbb/models/printer_model.dart';
|
import 'package:flutter_hbb/models/printer_model.dart';
|
||||||
import 'package:flutter_hbb/models/server_model.dart';
|
import 'package:flutter_hbb/models/server_model.dart';
|
||||||
import 'package:flutter_hbb/models/state_model.dart';
|
import 'package:flutter_hbb/models/state_model.dart';
|
||||||
import 'package:flutter_hbb/plugin/manager.dart';
|
|
||||||
import 'package:flutter_hbb/plugin/widgets/desktop_settings.dart';
|
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
@@ -55,7 +53,6 @@ enum SettingsTabKey {
|
|||||||
safety,
|
safety,
|
||||||
network,
|
network,
|
||||||
display,
|
display,
|
||||||
plugin,
|
|
||||||
account,
|
account,
|
||||||
printer,
|
printer,
|
||||||
about,
|
about,
|
||||||
@@ -64,7 +61,8 @@ enum SettingsTabKey {
|
|||||||
class DesktopSettingPage extends StatefulWidget {
|
class DesktopSettingPage extends StatefulWidget {
|
||||||
final SettingsTabKey initialTabkey;
|
final SettingsTabKey initialTabkey;
|
||||||
static final List<SettingsTabKey> tabKeys = [
|
static final List<SettingsTabKey> tabKeys = [
|
||||||
SettingsTabKey.general,
|
if (bind.mainGetBuildinOption(key: kOptionHideGeneralSetting) != 'Y')
|
||||||
|
SettingsTabKey.general,
|
||||||
if (!isWeb &&
|
if (!isWeb &&
|
||||||
!bind.isOutgoingOnly() &&
|
!bind.isOutgoingOnly() &&
|
||||||
!bind.isDisableSettings() &&
|
!bind.isDisableSettings() &&
|
||||||
@@ -74,10 +72,9 @@ class DesktopSettingPage extends StatefulWidget {
|
|||||||
bind.mainGetBuildinOption(key: kOptionHideNetworkSetting) != 'Y')
|
bind.mainGetBuildinOption(key: kOptionHideNetworkSetting) != 'Y')
|
||||||
SettingsTabKey.network,
|
SettingsTabKey.network,
|
||||||
if (!bind.isIncomingOnly()) SettingsTabKey.display,
|
if (!bind.isIncomingOnly()) SettingsTabKey.display,
|
||||||
if (!isWeb && !bind.isIncomingOnly() && bind.pluginFeatureIsEnabled())
|
|
||||||
SettingsTabKey.plugin,
|
|
||||||
if (!bind.isDisableAccount()) SettingsTabKey.account,
|
if (!bind.isDisableAccount()) SettingsTabKey.account,
|
||||||
if (isWindows &&
|
if (isWindows &&
|
||||||
|
!bind.isDisableSettings() &&
|
||||||
bind.mainGetBuildinOption(key: kOptionHideRemotePrinterSetting) != 'Y')
|
bind.mainGetBuildinOption(key: kOptionHideRemotePrinterSetting) != 'Y')
|
||||||
SettingsTabKey.printer,
|
SettingsTabKey.printer,
|
||||||
SettingsTabKey.about,
|
SettingsTabKey.about,
|
||||||
@@ -95,7 +92,8 @@ class DesktopSettingPage extends StatefulWidget {
|
|||||||
if (index == -1) {
|
if (index == -1) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (Get.isRegistered<PageController>(tag: _kSettingPageControllerTag)) {
|
if (Get.isRegistered<PageController>(tag: _kSettingPageControllerTag) &&
|
||||||
|
Get.isRegistered<Rx<SettingsTabKey>>(tag: _kSettingPageTabKeyTag)) {
|
||||||
DesktopTabPage.onAddSetting(initialPage: page);
|
DesktopTabPage.onAddSetting(initialPage: page);
|
||||||
PageController controller =
|
PageController controller =
|
||||||
Get.find<PageController>(tag: _kSettingPageControllerTag);
|
Get.find<PageController>(tag: _kSettingPageControllerTag);
|
||||||
@@ -163,17 +161,23 @@ class _DesktopSettingPageState extends State<DesktopSettingPage>
|
|||||||
if (!mounted) {
|
if (!mounted) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_canBeBlocked.value = await canBeBlocked();
|
final blocked = await canBeBlocked();
|
||||||
|
if (!mounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_canBeBlocked.value = blocked;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
super.dispose();
|
|
||||||
Get.delete<PageController>(tag: _kSettingPageControllerTag);
|
|
||||||
Get.delete<RxInt>(tag: _kSettingPageTabKeyTag);
|
|
||||||
WidgetsBinding.instance.removeObserver(this);
|
|
||||||
_videoConnTimer?.cancel();
|
_videoConnTimer?.cancel();
|
||||||
|
WidgetsBinding.instance.removeObserver(this);
|
||||||
|
Get.delete<PageController>(tag: _kSettingPageControllerTag);
|
||||||
|
Get.delete<Rx<SettingsTabKey>>(tag: _kSettingPageTabKeyTag);
|
||||||
|
// Get.delete does not dispose a plain ChangeNotifier.
|
||||||
|
controller.dispose();
|
||||||
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
List<_TabInfo> _settingTabs() {
|
List<_TabInfo> _settingTabs() {
|
||||||
@@ -196,10 +200,6 @@ class _DesktopSettingPageState extends State<DesktopSettingPage>
|
|||||||
settingTabs.add(_TabInfo(tab, 'Display',
|
settingTabs.add(_TabInfo(tab, 'Display',
|
||||||
Icons.desktop_windows_outlined, Icons.desktop_windows));
|
Icons.desktop_windows_outlined, Icons.desktop_windows));
|
||||||
break;
|
break;
|
||||||
case SettingsTabKey.plugin:
|
|
||||||
settingTabs.add(_TabInfo(
|
|
||||||
tab, 'Plugin', Icons.extension_outlined, Icons.extension));
|
|
||||||
break;
|
|
||||||
case SettingsTabKey.account:
|
case SettingsTabKey.account:
|
||||||
settingTabs.add(
|
settingTabs.add(
|
||||||
_TabInfo(tab, 'Account', Icons.person_outline, Icons.person));
|
_TabInfo(tab, 'Account', Icons.person_outline, Icons.person));
|
||||||
@@ -233,9 +233,6 @@ class _DesktopSettingPageState extends State<DesktopSettingPage>
|
|||||||
case SettingsTabKey.display:
|
case SettingsTabKey.display:
|
||||||
children.add(const _Display());
|
children.add(const _Display());
|
||||||
break;
|
break;
|
||||||
case SettingsTabKey.plugin:
|
|
||||||
children.add(const _Plugin());
|
|
||||||
break;
|
|
||||||
case SettingsTabKey.account:
|
case SettingsTabKey.account:
|
||||||
children.add(const _Account());
|
children.add(const _Account());
|
||||||
break;
|
break;
|
||||||
@@ -512,6 +509,15 @@ class _GeneralState extends State<_General> {
|
|||||||
kOptionOpenNewConnInTabs,
|
kOptionOpenNewConnInTabs,
|
||||||
isServer: false,
|
isServer: false,
|
||||||
),
|
),
|
||||||
|
Tooltip(
|
||||||
|
message: translate('port-forward-mux-tip'),
|
||||||
|
child: _OptionCheckBox(
|
||||||
|
context,
|
||||||
|
'Reuse one connection for port forwarding',
|
||||||
|
kOptionEnablePortForwardMux,
|
||||||
|
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)
|
if (isLinux)
|
||||||
Tooltip(
|
Tooltip(
|
||||||
@@ -566,6 +572,12 @@ class _GeneralState extends State<_General> {
|
|||||||
kOptionDirectxCapture,
|
kOptionDirectxCapture,
|
||||||
),
|
),
|
||||||
if (!isWeb && !incomingOnly) ...[
|
if (!isWeb && !incomingOnly) ...[
|
||||||
|
_OptionCheckBox(
|
||||||
|
context,
|
||||||
|
'Enable TCP hole punching',
|
||||||
|
kOptionEnableTcpPunch,
|
||||||
|
isServer: false,
|
||||||
|
),
|
||||||
_OptionCheckBox(
|
_OptionCheckBox(
|
||||||
context,
|
context,
|
||||||
'Enable UDP hole punching',
|
'Enable UDP hole punching',
|
||||||
@@ -579,6 +591,23 @@ class _GeneralState extends State<_General> {
|
|||||||
isServer: false,
|
isServer: false,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
if (!incomingOnly)
|
||||||
|
_OptionCheckBox(
|
||||||
|
context,
|
||||||
|
'Enable WebRTC P2P connection',
|
||||||
|
kOptionEnableWebrtc,
|
||||||
|
isServer: false,
|
||||||
|
),
|
||||||
|
if (!isWeb && !incomingOnly)
|
||||||
|
Tooltip(
|
||||||
|
message: translate('sync-clipboard-between-sessions-tip'),
|
||||||
|
child: _OptionCheckBox(
|
||||||
|
context,
|
||||||
|
'Sync clipboard between sessions',
|
||||||
|
kOptionAllowSyncClipboardBetweenSessions,
|
||||||
|
isServer: false,
|
||||||
|
),
|
||||||
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
// Add client-side wakelock option for desktop platforms
|
// Add client-side wakelock option for desktop platforms
|
||||||
@@ -591,10 +620,6 @@ class _GeneralState extends State<_General> {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isWeb && bind.mainShowOption(key: kOptionAllowLinuxHeadless)) {
|
|
||||||
children.add(_OptionCheckBox(
|
|
||||||
context, 'Allow linux headless', kOptionAllowLinuxHeadless));
|
|
||||||
}
|
|
||||||
if (!bind.isDisableAccount()) {
|
if (!bind.isDisableAccount()) {
|
||||||
children.add(_OptionCheckBox(
|
children.add(_OptionCheckBox(
|
||||||
context,
|
context,
|
||||||
@@ -2078,14 +2103,13 @@ 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 = getOtherDefaultSettingOption(key) == 'Y';
|
||||||
final isOptFixed = isOptionFixed(key);
|
final isOptFixed = isOtherDefaultSettingReadOnly(key);
|
||||||
onChanged(bool b) async {
|
onChanged(bool b) async {
|
||||||
await bind.mainSetUserDefaultOption(
|
await setOtherDefaultSettingOption(
|
||||||
key: key,
|
key,
|
||||||
value: b
|
b ? 'Y' : (key == kOptionEnableFileCopyPaste ? 'N' : defaultOptionNo),
|
||||||
? 'Y'
|
);
|
||||||
: (key == kOptionEnableFileCopyPaste ? 'N' : defaultOptionNo));
|
|
||||||
setState(() {});
|
setState(() {});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2255,51 +2279,6 @@ class _CheckboxState extends State<_Checkbox> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _Plugin extends StatefulWidget {
|
|
||||||
const _Plugin({Key? key}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<_Plugin> createState() => _PluginState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _PluginState extends State<_Plugin> {
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
bind.pluginListReload();
|
|
||||||
final scrollController = ScrollController();
|
|
||||||
return ChangeNotifierProvider.value(
|
|
||||||
value: pluginManager,
|
|
||||||
child: Consumer<PluginManager>(builder: (context, model, child) {
|
|
||||||
return ListView(
|
|
||||||
controller: scrollController,
|
|
||||||
children: model.plugins.map((entry) => pluginCard(entry)).toList(),
|
|
||||||
).marginOnly(bottom: _kListViewBottomMargin);
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget pluginCard(PluginInfo plugin) {
|
|
||||||
return ChangeNotifierProvider.value(
|
|
||||||
value: plugin,
|
|
||||||
child: Consumer<PluginInfo>(
|
|
||||||
builder: (context, model, child) => DesktopSettingsCard(plugin: model),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget accountAction() {
|
|
||||||
return Obx(() => _Button(
|
|
||||||
gFFI.userModel.userName.value.isEmpty
|
|
||||||
? 'Login'
|
|
||||||
: '${translate('Logout')} (${gFFI.userModel.accountLabelWithHandle})',
|
|
||||||
() => {
|
|
||||||
gFFI.userModel.userName.value.isEmpty
|
|
||||||
? loginDialog()
|
|
||||||
: logOutConfirmDialog()
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _Printer extends StatefulWidget {
|
class _Printer extends StatefulWidget {
|
||||||
const _Printer({super.key});
|
const _Printer({super.key});
|
||||||
|
|
||||||
|
|||||||
@@ -278,7 +278,39 @@ class _FileManagerPageState extends State<FileManagerPage>
|
|||||||
item.state != JobState.inProgress,
|
item.state != JobState.inProgress,
|
||||||
child: LinearPercentIndicator(
|
child: LinearPercentIndicator(
|
||||||
animateFromLastPercent: true,
|
animateFromLastPercent: true,
|
||||||
center: Text(item.percentText),
|
center: SizedBox.expand(
|
||||||
|
child: ShaderMask(
|
||||||
|
blendMode: BlendMode.srcATop,
|
||||||
|
shaderCallback: (bounds) =>
|
||||||
|
LinearGradient(
|
||||||
|
colors: [
|
||||||
|
Colors.white,
|
||||||
|
Colors.transparent,
|
||||||
|
],
|
||||||
|
stops: [item.percent, item.percent],
|
||||||
|
).createShader(bounds),
|
||||||
|
child: FittedBox(
|
||||||
|
fit: BoxFit.scaleDown,
|
||||||
|
child: Text.rich(
|
||||||
|
TextSpan(
|
||||||
|
text: item.percentText,
|
||||||
|
children: [
|
||||||
|
if (item.recvJobRes)
|
||||||
|
TextSpan(
|
||||||
|
text:
|
||||||
|
' ${readableFileSize(item.speed)}/s',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w300,
|
||||||
|
color: MyTheme.darkGray,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
barRadius: Radius.circular(15),
|
barRadius: Radius.circular(15),
|
||||||
percent: item.percent,
|
percent: item.percent,
|
||||||
progressColor: MyTheme.accent,
|
progressColor: MyTheme.accent,
|
||||||
@@ -1094,6 +1126,7 @@ class _FileManagerViewState extends State<FileManagerView> {
|
|||||||
return element.name.contains(_searchText.value);
|
return element.name.contains(_searchText.value);
|
||||||
}).toList(growable: false)
|
}).toList(growable: false)
|
||||||
: entries;
|
: entries;
|
||||||
|
// Keep rows lazy so large directories only build visible list items.
|
||||||
final rows = filteredEntries.map((entry) {
|
final rows = filteredEntries.map((entry) {
|
||||||
final sizeStr =
|
final sizeStr =
|
||||||
entry.isFile ? readableFileSize(entry.size.toDouble()) : "";
|
entry.isFile ? readableFileSize(entry.size.toDouble()) : "";
|
||||||
@@ -1276,7 +1309,7 @@ class _FileManagerViewState extends State<FileManagerView> {
|
|||||||
],
|
],
|
||||||
))),
|
))),
|
||||||
);
|
);
|
||||||
}).toList(growable: false);
|
});
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
@@ -1292,7 +1325,7 @@ class _FileManagerViewState extends State<FileManagerView> {
|
|||||||
controller: scrollController,
|
controller: scrollController,
|
||||||
itemExtent: kDesktopFileTransferRowHeight,
|
itemExtent: kDesktopFileTransferRowHeight,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
return rows[index];
|
return rows.elementAt(index);
|
||||||
},
|
},
|
||||||
itemCount: rows.length,
|
itemCount: rows.length,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ import '../../common/shared_state.dart';
|
|||||||
import '../../utils/image.dart';
|
import '../../utils/image.dart';
|
||||||
import '../widgets/remote_toolbar.dart';
|
import '../widgets/remote_toolbar.dart';
|
||||||
import '../widgets/kb_layout_type_chooser.dart';
|
import '../widgets/kb_layout_type_chooser.dart';
|
||||||
import '../widgets/raster_stall_monitor.dart';
|
|
||||||
import '../widgets/tabbar_widget.dart';
|
import '../widgets/tabbar_widget.dart';
|
||||||
import 'macos_full_screen_focus_recovery.dart';
|
import 'macos_full_screen_focus_recovery.dart';
|
||||||
|
|
||||||
@@ -157,7 +156,6 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
widget.tabController?.state.listen(_onMacOSTabStateChanged);
|
widget.tabController?.state.listen(_onMacOSTabStateChanged);
|
||||||
}
|
}
|
||||||
Get.put<FFI>(_ffi, tag: widget.id);
|
Get.put<FFI>(_ffi, tag: widget.id);
|
||||||
RasterStallMonitor.start();
|
|
||||||
_ffi.imageModel.addCallbackOnFirstImage((String peerId) {
|
_ffi.imageModel.addCallbackOnFirstImage((String peerId) {
|
||||||
_ffi.canvasModel.activateLocalCursor();
|
_ffi.canvasModel.activateLocalCursor();
|
||||||
showKBLayoutTypeChooserIfNeeded(
|
showKBLayoutTypeChooserIfNeeded(
|
||||||
@@ -184,7 +182,6 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
WakelockManager.enable(_uniqueKey);
|
WakelockManager.enable(_uniqueKey);
|
||||||
|
|
||||||
_ffi.ffiModel.updateEventListener(sessionId, widget.id);
|
_ffi.ffiModel.updateEventListener(sessionId, widget.id);
|
||||||
if (!isWeb) bind.pluginSyncUi(syncTo: kAppTypeDesktopRemote);
|
|
||||||
_ffi.qualityMonitorModel.checkShowQualityMonitor(sessionId);
|
_ffi.qualityMonitorModel.checkShowQualityMonitor(sessionId);
|
||||||
_ffi.dialogManager.loadMobileActionsOverlayVisible();
|
_ffi.dialogManager.loadMobileActionsOverlayVisible();
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
@@ -276,6 +273,11 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
tabState.tabs[selected].key == widget.id;
|
tabState.tabs[selected].key == widget.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Every Windows requestFocus() must pass this, or a blocking dialog or an
|
||||||
|
// inactive tab could hand remote input to this page.
|
||||||
|
bool get _windowsCanFocusRemoteInput =>
|
||||||
|
_isSelectedTab && _blockableOverlayState.middleBlocked.isFalse;
|
||||||
|
|
||||||
bool get _isMacOSKeyboardContextActive {
|
bool get _isMacOSKeyboardContextActive {
|
||||||
return stateGlobal.isFocused.value && !_isWindowBlur && _isSelectedTab;
|
return stateGlobal.isFocused.value && !_isWindowBlur && _isSelectedTab;
|
||||||
}
|
}
|
||||||
@@ -516,6 +518,15 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
_queueMacOSKeyboardAfterFullScreen(allowHiddenLifecycle: true);
|
_queueMacOSKeyboardAfterFullScreen(allowHiddenLifecycle: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Refocus without PointerEnter: the cursor already hovers the image when
|
||||||
|
// focus returns (Alt+Tab, taskbar), so enterView() never fires again.
|
||||||
|
if (isWindows &&
|
||||||
|
_cursorOverImage.value &&
|
||||||
|
_windowsCanFocusRemoteInput &&
|
||||||
|
!_rawKeyFocusNode.hasFocus) {
|
||||||
|
_rawKeyFocusNode.requestFocus();
|
||||||
|
}
|
||||||
|
|
||||||
// Restore relative mouse mode constraints when window regains focus.
|
// Restore relative mouse mode constraints when window regains focus.
|
||||||
if (_ffi.inputModel.relativeMouseMode.value) {
|
if (_ffi.inputModel.relativeMouseMode.value) {
|
||||||
if (isMacOS) {
|
if (isMacOS) {
|
||||||
@@ -526,7 +537,7 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
_cursorOverImage.value = true;
|
_cursorOverImage.value = true;
|
||||||
_macOSLocalFocusLost = false;
|
_macOSLocalFocusLost = false;
|
||||||
}
|
}
|
||||||
} else {
|
} else if (!isWindows || _windowsCanFocusRemoteInput) {
|
||||||
_rawKeyFocusNode.requestFocus();
|
_rawKeyFocusNode.requestFocus();
|
||||||
}
|
}
|
||||||
_ffi.inputModel.onWindowFocus();
|
_ffi.inputModel.onWindowFocus();
|
||||||
@@ -649,7 +660,7 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
// Clear callback reference to prevent memory leaks and stale references
|
// Clear callback reference to prevent memory leaks and stale references
|
||||||
_ffi.inputModel.onRelativeMouseModeDisabled = null;
|
_ffi.inputModel.onRelativeMouseModeDisabled = null;
|
||||||
// Relative mouse mode cleanup is centralized in FFI.close(closeSession: ...).
|
// Relative mouse mode cleanup is centralized in FFI.close(closeSession: ...).
|
||||||
_ffi.textureModel.onRemotePageDispose();
|
_ffi.textureModel.onRemotePageDispose(closeSession);
|
||||||
if (closeSession && !isMacOS) {
|
if (closeSession && !isMacOS) {
|
||||||
// ensure we leave this session, this is a double check
|
// ensure we leave this session, this is a double check
|
||||||
// enterOrLeave() is already called previously in _releaseMacOSRemoteInput() for macOS.
|
// enterOrLeave() is already called previously in _releaseMacOSRemoteInput() for macOS.
|
||||||
@@ -838,7 +849,16 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
_macOSLocalFocusLost = false;
|
_macOSLocalFocusLost = false;
|
||||||
stateGlobal.getInputSource(force: true);
|
stateGlobal.getInputSource(force: true);
|
||||||
_syncMacOSKeyboardGrab(reassert: true, allowInactiveLifecycle: true);
|
_syncMacOSKeyboardGrab(reassert: true, allowInactiveLifecycle: true);
|
||||||
} else if (!isWindows) {
|
} else if (isWindows) {
|
||||||
|
// Blur unfocuses this node and nothing restores it, so the keyboard stayed
|
||||||
|
// dead until a click. Focus only while the window is really active, or a
|
||||||
|
// background window would grab system keys. onFocusChange does enterOrLeave.
|
||||||
|
if (!_isWindowBlur &&
|
||||||
|
_windowsCanFocusRemoteInput &&
|
||||||
|
!_rawKeyFocusNode.hasFocus) {
|
||||||
|
_rawKeyFocusNode.requestFocus();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
if (!_rawKeyFocusNode.hasFocus) {
|
if (!_rawKeyFocusNode.hasFocus) {
|
||||||
_rawKeyFocusNode.requestFocus();
|
_rawKeyFocusNode.requestFocus();
|
||||||
}
|
}
|
||||||
@@ -1404,4 +1424,3 @@ class CursorPaint extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,14 @@ import '../../models/file_model.dart';
|
|||||||
import '../../models/platform_model.dart';
|
import '../../models/platform_model.dart';
|
||||||
import '../../models/server_model.dart';
|
import '../../models/server_model.dart';
|
||||||
|
|
||||||
|
/// Set only by this window's own close control, and only once the user has confirmed. Any other
|
||||||
|
/// way the window can go - a session logout closing every window, the window manager, a native
|
||||||
|
/// title-bar button this app does not draw - leaves it false, which is the honest answer:
|
||||||
|
/// nothing in that close says who asked for it. It lives at file scope because the control that
|
||||||
|
/// sets it (`ConnectionManagerState`) and the handler that reads it (`_DesktopServerPageState`)
|
||||||
|
/// are different widgets.
|
||||||
|
bool _cmClosedByOperator = false;
|
||||||
|
|
||||||
class DesktopServerPage extends StatefulWidget {
|
class DesktopServerPage extends StatefulWidget {
|
||||||
const DesktopServerPage({Key? key}) : super(key: key);
|
const DesktopServerPage({Key? key}) : super(key: key);
|
||||||
|
|
||||||
@@ -55,7 +63,10 @@ class _DesktopServerPageState extends State<DesktopServerPage>
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void onWindowClose() {
|
void onWindowClose() {
|
||||||
Future.wait([gFFI.serverModel.closeAll(), gFFI.close()]).then((_) {
|
// Other platforms keep the old behaviour exactly: the ambiguity this guards against is a
|
||||||
|
// Linux session logout, which closes every window in the session.
|
||||||
|
final byOperator = _cmClosedByOperator || !isLinux;
|
||||||
|
Future.wait([gFFI.serverModel.closeAll(byOperator: byOperator), gFFI.close()]).then((_) {
|
||||||
if (isMacOS) {
|
if (isMacOS) {
|
||||||
RdPlatformChannel.instance.terminate();
|
RdPlatformChannel.instance.terminate();
|
||||||
} else {
|
} else {
|
||||||
@@ -327,6 +338,7 @@ class ConnectionManagerState extends State<ConnectionManager>
|
|||||||
var tabController = gFFI.serverModel.tabController;
|
var tabController = gFFI.serverModel.tabController;
|
||||||
final connLength = tabController.length;
|
final connLength = tabController.length;
|
||||||
if (connLength <= 1) {
|
if (connLength <= 1) {
|
||||||
|
_cmClosedByOperator = true;
|
||||||
windowManager.close();
|
windowManager.close();
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
@@ -338,6 +350,9 @@ class ConnectionManagerState extends State<ConnectionManager>
|
|||||||
res = await closeConfirmDialog();
|
res = await closeConfirmDialog();
|
||||||
}
|
}
|
||||||
if (res) {
|
if (res) {
|
||||||
|
// After the dialog, never before it: an external close while it is open must not
|
||||||
|
// inherit an intent the user had not expressed yet.
|
||||||
|
_cmClosedByOperator = true;
|
||||||
windowManager.close();
|
windowManager.close();
|
||||||
}
|
}
|
||||||
return res;
|
return res;
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import 'package:flutter_hbb/common.dart';
|
|||||||
import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
|
import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
|
||||||
import 'package:flutter_hbb/models/model.dart';
|
import 'package:flutter_hbb/models/model.dart';
|
||||||
import 'package:flutter_hbb/models/terminal_model.dart';
|
import 'package:flutter_hbb/models/terminal_model.dart';
|
||||||
import 'package:xterm/xterm.dart';
|
import 'package:flutter_hbb/models/terminal_mouse_handler.dart';
|
||||||
import 'terminal_connection_manager.dart';
|
import 'terminal_connection_manager.dart';
|
||||||
|
|
||||||
class TerminalPage extends StatefulWidget {
|
class TerminalPage extends StatefulWidget {
|
||||||
@@ -19,6 +19,8 @@ class TerminalPage extends StatefulWidget {
|
|||||||
required this.tabKey,
|
required this.tabKey,
|
||||||
this.forceRelay,
|
this.forceRelay,
|
||||||
this.connToken,
|
this.connToken,
|
||||||
|
this.onClipboardWriteBlocked,
|
||||||
|
this.onClipboardWriteSucceeded,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
final String id;
|
final String id;
|
||||||
final String? password;
|
final String? password;
|
||||||
@@ -26,6 +28,8 @@ class TerminalPage extends StatefulWidget {
|
|||||||
final bool? forceRelay;
|
final bool? forceRelay;
|
||||||
final bool? isSharedPassword;
|
final bool? isSharedPassword;
|
||||||
final String? connToken;
|
final String? connToken;
|
||||||
|
final ValueChanged<String>? onClipboardWriteBlocked;
|
||||||
|
final ValueChanged<String>? onClipboardWriteSucceeded;
|
||||||
final int terminalId;
|
final int terminalId;
|
||||||
|
|
||||||
/// Tab key for focus management, passed from parent to avoid duplicate construction
|
/// Tab key for focus management, passed from parent to avoid duplicate construction
|
||||||
@@ -71,6 +75,8 @@ class _TerminalPageState extends State<TerminalPage>
|
|||||||
|
|
||||||
// Create terminal model with specific terminal ID
|
// Create terminal model with specific terminal ID
|
||||||
_terminalModel = TerminalModel(_ffi, widget.terminalId);
|
_terminalModel = TerminalModel(_ffi, widget.terminalId);
|
||||||
|
_terminalModel.onClipboardWriteBlocked = widget.onClipboardWriteBlocked;
|
||||||
|
_terminalModel.onClipboardWriteSucceeded = widget.onClipboardWriteSucceeded;
|
||||||
debugPrint(
|
debugPrint(
|
||||||
'[TerminalPage] Terminal model created for terminal ${widget.terminalId}');
|
'[TerminalPage] Terminal model created for terminal ${widget.terminalId}');
|
||||||
|
|
||||||
@@ -197,7 +203,7 @@ class _TerminalPageState extends State<TerminalPage>
|
|||||||
body: LayoutBuilder(
|
body: LayoutBuilder(
|
||||||
builder: (context, constraints) {
|
builder: (context, constraints) {
|
||||||
final heightPx = constraints.maxHeight;
|
final heightPx = constraints.maxHeight;
|
||||||
return TerminalView(
|
return TerminalMouseInteraction(
|
||||||
_terminalModel.terminal,
|
_terminalModel.terminal,
|
||||||
controller: _terminalModel.terminalController,
|
controller: _terminalModel.terminalController,
|
||||||
focusNode: _terminalFocusNode,
|
focusNode: _terminalFocusNode,
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:desktop_multi_window/desktop_multi_window.dart';
|
import 'package:desktop_multi_window/desktop_multi_window.dart';
|
||||||
@@ -10,6 +11,8 @@ import 'package:flutter_hbb/models/state_model.dart';
|
|||||||
import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
|
import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
|
||||||
import 'package:flutter_hbb/utils/multi_window_manager.dart';
|
import 'package:flutter_hbb/utils/multi_window_manager.dart';
|
||||||
import 'package:flutter_hbb/models/model.dart';
|
import 'package:flutter_hbb/models/model.dart';
|
||||||
|
import 'package:flutter_hbb/models/terminal_copy_shortcut.dart';
|
||||||
|
import 'package:flutter_hbb/models/terminal_model.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
|
||||||
import '../../models/platform_model.dart';
|
import '../../models/platform_model.dart';
|
||||||
@@ -19,6 +22,12 @@ import '../widgets/material_mod_popup_menu.dart' as mod_menu;
|
|||||||
import '../widgets/popup_menu.dart';
|
import '../widgets/popup_menu.dart';
|
||||||
import 'package:bot_toast/bot_toast.dart';
|
import 'package:bot_toast/bot_toast.dart';
|
||||||
|
|
||||||
|
typedef _TerminalClipboardSource = ({
|
||||||
|
String peerId,
|
||||||
|
int terminalId,
|
||||||
|
String tabKey,
|
||||||
|
});
|
||||||
|
|
||||||
class TerminalTabPage extends StatefulWidget {
|
class TerminalTabPage extends StatefulWidget {
|
||||||
final Map<String, dynamic> params;
|
final Map<String, dynamic> params;
|
||||||
|
|
||||||
@@ -30,6 +39,18 @@ class TerminalTabPage extends StatefulWidget {
|
|||||||
|
|
||||||
class _TerminalTabPageState extends State<TerminalTabPage> {
|
class _TerminalTabPageState extends State<TerminalTabPage> {
|
||||||
DesktopTabController get tabController => Get.find<DesktopTabController>();
|
DesktopTabController get tabController => Get.find<DesktopTabController>();
|
||||||
|
bool get _canConfigureTerminalClipboardPermission =>
|
||||||
|
canConfigureTerminalClipboardPermission(
|
||||||
|
settingsDisabled: bind.isDisableSettings(),
|
||||||
|
optionFixed: isOptionFixed(kOptionAllowTerminalClipboardWrite),
|
||||||
|
);
|
||||||
|
bool get _canHandleTerminalClipboardWriteRequest =>
|
||||||
|
canHandleTerminalClipboardWriteRequest(
|
||||||
|
localOption: bind.mainGetLocalOption(
|
||||||
|
key: kOptionAllowTerminalClipboardWrite,
|
||||||
|
),
|
||||||
|
canConfigurePermission: _canConfigureTerminalClipboardPermission,
|
||||||
|
);
|
||||||
|
|
||||||
static const IconData selectedIcon = Icons.terminal;
|
static const IconData selectedIcon = Icons.terminal;
|
||||||
static const IconData unselectedIcon = Icons.terminal_outlined;
|
static const IconData unselectedIcon = Icons.terminal_outlined;
|
||||||
@@ -38,6 +59,9 @@ class _TerminalTabPageState extends State<TerminalTabPage> {
|
|||||||
final Set<String> _closingTabs = {};
|
final Set<String> _closingTabs = {};
|
||||||
// When true, all session cleanup should persist (window-level close in progress)
|
// When true, all session cleanup should persist (window-level close in progress)
|
||||||
bool _windowClosing = false;
|
bool _windowClosing = false;
|
||||||
|
CancelFunc? _terminalClipboardNoticeCancel;
|
||||||
|
final _terminalClipboardNotice =
|
||||||
|
TerminalClipboardNoticeCoordinator<_TerminalClipboardSource>();
|
||||||
|
|
||||||
_TerminalTabPageState(Map<String, dynamic> params) {
|
_TerminalTabPageState(Map<String, dynamic> params) {
|
||||||
Get.put(DesktopTabController(tabType: DesktopTabType.terminal));
|
Get.put(DesktopTabController(tabType: DesktopTabType.terminal));
|
||||||
@@ -45,7 +69,10 @@ class _TerminalTabPageState extends State<TerminalTabPage> {
|
|||||||
WindowController.fromWindowId(windowId())
|
WindowController.fromWindowId(windowId())
|
||||||
.setTitle(getWindowNameWithId(id));
|
.setTitle(getWindowNameWithId(id));
|
||||||
};
|
};
|
||||||
tabController.onRemoved = (_, id) => onRemoveId(id);
|
tabController.onRemoved = (_, id) {
|
||||||
|
_closeTerminalClipboardNoticeForTab(id);
|
||||||
|
onRemoveId(id);
|
||||||
|
};
|
||||||
tabController.onCloseWindow = _closeWindowFromConnection;
|
tabController.onCloseWindow = _closeWindowFromConnection;
|
||||||
final terminalId = params['terminalId'] ?? _nextTerminalId++;
|
final terminalId = params['terminalId'] ?? _nextTerminalId++;
|
||||||
tabController.add(_createTerminalTab(
|
tabController.add(_createTerminalTab(
|
||||||
@@ -70,6 +97,11 @@ class _TerminalTabPageState extends State<TerminalTabPage> {
|
|||||||
final alias = bind.mainGetPeerOptionSync(id: peerId, key: 'alias');
|
final alias = bind.mainGetPeerOptionSync(id: peerId, key: 'alias');
|
||||||
final tabLabel =
|
final tabLabel =
|
||||||
alias.isNotEmpty ? '$alias #$terminalId' : '$peerId #$terminalId';
|
alias.isNotEmpty ? '$alias #$terminalId' : '$peerId #$terminalId';
|
||||||
|
final clipboardSource = (
|
||||||
|
peerId: peerId,
|
||||||
|
terminalId: terminalId,
|
||||||
|
tabKey: tabKey,
|
||||||
|
);
|
||||||
return TabInfo(
|
return TabInfo(
|
||||||
key: tabKey,
|
key: tabKey,
|
||||||
label: tabLabel,
|
label: tabLabel,
|
||||||
@@ -86,10 +118,169 @@ class _TerminalTabPageState extends State<TerminalTabPage> {
|
|||||||
tabController: tabController,
|
tabController: tabController,
|
||||||
forceRelay: forceRelay,
|
forceRelay: forceRelay,
|
||||||
connToken: connToken,
|
connToken: connToken,
|
||||||
|
onClipboardWriteBlocked: _canHandleTerminalClipboardWriteRequest
|
||||||
|
? (text) => _handleTerminalClipboardWriteBlocked(
|
||||||
|
clipboardSource,
|
||||||
|
text,
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
onClipboardWriteSucceeded: (_) {
|
||||||
|
_handleTerminalClipboardWriteSucceeded(clipboardSource);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _handleTerminalClipboardWriteBlocked(
|
||||||
|
_TerminalClipboardSource source,
|
||||||
|
String clipboardText,
|
||||||
|
) {
|
||||||
|
if (!mounted) return;
|
||||||
|
final option = bind.mainGetLocalOption(
|
||||||
|
key: kOptionAllowTerminalClipboardWrite,
|
||||||
|
);
|
||||||
|
final request = _terminalClipboardNotice.recordBlocked(
|
||||||
|
source: source,
|
||||||
|
text: clipboardText,
|
||||||
|
option: option,
|
||||||
|
canWrite: _canWriteTerminalClipboard,
|
||||||
|
);
|
||||||
|
if (request != null) _showTerminalClipboardNotice(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showTerminalClipboardNotice(
|
||||||
|
TerminalClipboardNoticeRequest<_TerminalClipboardSource> request,
|
||||||
|
) {
|
||||||
|
_terminalClipboardNoticeCancel = BotToast.showCustomNotification(
|
||||||
|
duration: null,
|
||||||
|
enableSlideOff: false,
|
||||||
|
onlyOne: true,
|
||||||
|
onClose: _handleTerminalClipboardNoticeClosed,
|
||||||
|
toastBuilder: (_) => AnimatedBuilder(
|
||||||
|
animation: _terminalClipboardNotice,
|
||||||
|
builder: (_, __) => MaterialBanner(
|
||||||
|
leading: const Icon(Icons.content_copy_outlined),
|
||||||
|
content: Text(translate(kTerminalClipboardNoticeMessageKey)),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: _terminalClipboardNotice.canClaimAction
|
||||||
|
? _handleTerminalClipboardNegativeAction
|
||||||
|
: null,
|
||||||
|
child: Text(translate(request.negativeActionKey)),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: _terminalClipboardNotice.canClaimAction
|
||||||
|
? _handleTerminalClipboardPositiveAction
|
||||||
|
: null,
|
||||||
|
child: Text(translate(request.actionKey)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleTerminalClipboardNegativeAction() {
|
||||||
|
final request = _terminalClipboardNotice.claimCurrentAction();
|
||||||
|
if (request == null) return;
|
||||||
|
if (request.persistAllowed) {
|
||||||
|
unawaited(_declineTerminalClipboardWrite());
|
||||||
|
} else {
|
||||||
|
_closeTerminalClipboardNotice();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleTerminalClipboardPositiveAction() {
|
||||||
|
final request = _terminalClipboardNotice.claimCurrentAction();
|
||||||
|
if (request == null) return;
|
||||||
|
unawaited(_completeTerminalClipboardWrite(request));
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleTerminalClipboardNoticeClosed() {
|
||||||
|
_terminalClipboardNoticeCancel = null;
|
||||||
|
_terminalClipboardNotice.noticeClosed();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _canWriteTerminalClipboard(
|
||||||
|
_TerminalClipboardSource source,
|
||||||
|
) {
|
||||||
|
if (!_canHandleTerminalClipboardWriteRequest) return false;
|
||||||
|
final ffi = TerminalConnectionManager.getExistingConnection(source.peerId);
|
||||||
|
return ffi != null &&
|
||||||
|
!ffi.closed &&
|
||||||
|
ffi.ffiModel.permissions['clipboard'] != false &&
|
||||||
|
tabController.state.value.tabs.any((tab) => tab.key == source.tabKey) &&
|
||||||
|
ffi.terminalModels.containsKey(source.terminalId);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleTerminalClipboardWriteSucceeded(
|
||||||
|
_TerminalClipboardSource source,
|
||||||
|
) {
|
||||||
|
final request = _terminalClipboardNotice.currentForSource(source);
|
||||||
|
if (request == null) return;
|
||||||
|
_closeTerminalClipboardNotice();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _declineTerminalClipboardWrite() async {
|
||||||
|
try {
|
||||||
|
await bind.mainSetLocalOption(
|
||||||
|
key: kOptionAllowTerminalClipboardWrite,
|
||||||
|
value: kTerminalClipboardWriteDenied,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
debugPrint(
|
||||||
|
'[TerminalTabPage] Failed to save terminal clipboard permission: $error');
|
||||||
|
return;
|
||||||
|
} finally {
|
||||||
|
_terminalClipboardNotice.releaseAction();
|
||||||
|
}
|
||||||
|
_closeTerminalClipboardNotice();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _completeTerminalClipboardWrite(
|
||||||
|
TerminalClipboardNoticeRequest<_TerminalClipboardSource> request,
|
||||||
|
) async {
|
||||||
|
final source = request.source;
|
||||||
|
var completed = false;
|
||||||
|
try {
|
||||||
|
completed = await completeTerminalClipboardWrite(
|
||||||
|
clipboardText: request.text,
|
||||||
|
canWrite: () => _canWriteTerminalClipboard(source),
|
||||||
|
writeClipboard: writeTerminalClipboard,
|
||||||
|
persistAllowed: request.persistAllowed
|
||||||
|
? () => bind.mainSetLocalOption(
|
||||||
|
key: kOptionAllowTerminalClipboardWrite,
|
||||||
|
value: kTerminalClipboardWriteAllowed,
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
debugPrint(
|
||||||
|
'[TerminalTabPage] Failed to complete terminal clipboard write: $error');
|
||||||
|
} finally {
|
||||||
|
_terminalClipboardNotice.releaseAction();
|
||||||
|
}
|
||||||
|
if (!completed) return;
|
||||||
|
_closeTerminalClipboardNotice();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _closeTerminalClipboardNoticeForTab(String tabKey) {
|
||||||
|
final current = _terminalClipboardNotice.current;
|
||||||
|
if (current?.source.tabKey != tabKey) return;
|
||||||
|
_closeTerminalClipboardNotice();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _closeTerminalClipboardNotice() {
|
||||||
|
if (!_terminalClipboardNotice.beginClose()) return;
|
||||||
|
final cancel = _terminalClipboardNoticeCancel;
|
||||||
|
if (cancel == null) {
|
||||||
|
debugPrint('[TerminalTabPage] Clipboard notice controller is missing');
|
||||||
|
_terminalClipboardNotice.noticeClosed();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
cancel();
|
||||||
|
}
|
||||||
|
|
||||||
/// Unified tab close handler for all close paths (button, shortcut, programmatic).
|
/// Unified tab close handler for all close paths (button, shortcut, programmatic).
|
||||||
/// Shows audit dialog, cleans up session if not persistent, then removes the UI tab.
|
/// Shows audit dialog, cleans up session if not persistent, then removes the UI tab.
|
||||||
Future<void> _closeTab(String tabKey) async {
|
Future<void> _closeTab(String tabKey) async {
|
||||||
@@ -147,6 +338,8 @@ class _TerminalTabPageState extends State<TerminalTabPage> {
|
|||||||
// Remove all UI tabs immediately (same instant behavior as the old tabController.clear())
|
// Remove all UI tabs immediately (same instant behavior as the old tabController.clear())
|
||||||
// Keep the cleanup target lookup below synchronous before its first await:
|
// Keep the cleanup target lookup below synchronous before its first await:
|
||||||
// it relies on the current frame still retaining each TerminalPage's FFI/model.
|
// it relies on the current frame still retaining each TerminalPage's FFI/model.
|
||||||
|
_terminalClipboardNotice.clear();
|
||||||
|
_terminalClipboardNoticeCancel?.call();
|
||||||
tabController.clear();
|
tabController.clear();
|
||||||
// Run session cleanup in parallel with bounded timeout (closeTerminal() has internal 3s timeout).
|
// Run session cleanup in parallel with bounded timeout (closeTerminal() has internal 3s timeout).
|
||||||
// Skip tabs already being closed by a concurrent _closeTab() to avoid duplicate FFI calls.
|
// Skip tabs already being closed by a concurrent _closeTab() to avoid duplicate FFI calls.
|
||||||
@@ -357,6 +550,8 @@ class _TerminalTabPageState extends State<TerminalTabPage> {
|
|||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
HardwareKeyboard.instance.removeHandler(_handleKeyEvent);
|
HardwareKeyboard.instance.removeHandler(_handleKeyEvent);
|
||||||
|
_terminalClipboardNotice.clear();
|
||||||
|
_terminalClipboardNoticeCancel?.call();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ import '../../common/shared_state.dart';
|
|||||||
import '../../utils/image.dart';
|
import '../../utils/image.dart';
|
||||||
import '../widgets/remote_toolbar.dart';
|
import '../widgets/remote_toolbar.dart';
|
||||||
import '../widgets/kb_layout_type_chooser.dart';
|
import '../widgets/kb_layout_type_chooser.dart';
|
||||||
import '../widgets/raster_stall_monitor.dart';
|
|
||||||
import '../widgets/tabbar_widget.dart';
|
import '../widgets/tabbar_widget.dart';
|
||||||
|
|
||||||
import 'package:flutter_hbb/native/custom_cursor.dart'
|
import 'package:flutter_hbb/native/custom_cursor.dart'
|
||||||
@@ -103,7 +102,6 @@ class _ViewCameraPageState extends State<ViewCameraPage>
|
|||||||
super.initState();
|
super.initState();
|
||||||
_ffi = FFI(widget.sessionId);
|
_ffi = FFI(widget.sessionId);
|
||||||
Get.put<FFI>(_ffi, tag: widget.id);
|
Get.put<FFI>(_ffi, tag: widget.id);
|
||||||
RasterStallMonitor.start();
|
|
||||||
_ffi.imageModel.addCallbackOnFirstImage((String peerId) {
|
_ffi.imageModel.addCallbackOnFirstImage((String peerId) {
|
||||||
showKBLayoutTypeChooserIfNeeded(
|
showKBLayoutTypeChooserIfNeeded(
|
||||||
_ffi.ffiModel.pi.platform, _ffi.dialogManager);
|
_ffi.ffiModel.pi.platform, _ffi.dialogManager);
|
||||||
@@ -129,7 +127,6 @@ class _ViewCameraPageState extends State<ViewCameraPage>
|
|||||||
WakelockManager.enable(_uniqueKey);
|
WakelockManager.enable(_uniqueKey);
|
||||||
|
|
||||||
_ffi.ffiModel.updateEventListener(sessionId, widget.id);
|
_ffi.ffiModel.updateEventListener(sessionId, widget.id);
|
||||||
if (!isWeb) bind.pluginSyncUi(syncTo: kAppTypeDesktopRemote);
|
|
||||||
_ffi.qualityMonitorModel.checkShowQualityMonitor(sessionId);
|
_ffi.qualityMonitorModel.checkShowQualityMonitor(sessionId);
|
||||||
_ffi.dialogManager.loadMobileActionsOverlayVisible();
|
_ffi.dialogManager.loadMobileActionsOverlayVisible();
|
||||||
DesktopMultiWindow.addListener(this);
|
DesktopMultiWindow.addListener(this);
|
||||||
@@ -224,7 +221,7 @@ class _ViewCameraPageState extends State<ViewCameraPage>
|
|||||||
// https://github.com/flutter/flutter/issues/64935
|
// https://github.com/flutter/flutter/issues/64935
|
||||||
super.dispose();
|
super.dispose();
|
||||||
debugPrint("VIEW CAMERA PAGE dispose session $sessionId ${widget.id}");
|
debugPrint("VIEW CAMERA PAGE dispose session $sessionId ${widget.id}");
|
||||||
_ffi.textureModel.onViewCameraPageDispose();
|
_ffi.textureModel.onViewCameraPageDispose(closeSession);
|
||||||
if (closeSession) {
|
if (closeSession) {
|
||||||
// ensure we leave this session, this is a double check
|
// ensure we leave this session, this is a double check
|
||||||
_ffi.inputModel.enterOrLeave(false);
|
_ffi.inputModel.enterOrLeave(false);
|
||||||
|
|||||||
@@ -1,52 +0,0 @@
|
|||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:flutter/scheduler.dart';
|
|
||||||
|
|
||||||
import '../../common.dart';
|
|
||||||
import '../../consts.dart';
|
|
||||||
import '../../models/platform_model.dart';
|
|
||||||
import '../../models/state_model.dart';
|
|
||||||
|
|
||||||
/// Records a hung raster thread (frames continuously scheduled but no frame
|
|
||||||
/// timings delivered for 30s) in `texture-render-health`; a hang cannot be
|
|
||||||
/// rescued in-process, so the next launch defaults texture rendering off.
|
|
||||||
class RasterStallMonitor {
|
|
||||||
static bool _started = false;
|
|
||||||
static bool _reported = false;
|
|
||||||
static DateTime? _lastTimings;
|
|
||||||
static DateTime _lastQuiet = DateTime.now();
|
|
||||||
|
|
||||||
static void start() {
|
|
||||||
if (_started || isWeb) return;
|
|
||||||
_started = true;
|
|
||||||
SchedulerBinding.instance.addTimingsCallback((_) {
|
|
||||||
_lastTimings = DateTime.now();
|
|
||||||
});
|
|
||||||
Timer.periodic(const Duration(seconds: 2), (_) {
|
|
||||||
if (_reported) return;
|
|
||||||
final now = DateTime.now();
|
|
||||||
final lifecycle = SchedulerBinding.instance.lifecycleState;
|
|
||||||
// Minimized/inactive (incl. screen lock) or idle (nothing scheduled):
|
|
||||||
// no timings is legitimate, keep moving the quiet anchor forward.
|
|
||||||
if (stateGlobal.isMinimized ||
|
|
||||||
(lifecycle != null && lifecycle != AppLifecycleState.resumed) ||
|
|
||||||
!SchedulerBinding.instance.hasScheduledFrame) {
|
|
||||||
_lastQuiet = now;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var ref = _lastQuiet;
|
|
||||||
final lastTimings = _lastTimings;
|
|
||||||
if (lastTimings != null && lastTimings.isAfter(ref)) {
|
|
||||||
ref = lastTimings;
|
|
||||||
}
|
|
||||||
if (now.difference(ref) > const Duration(seconds: 30)) {
|
|
||||||
_reported = true;
|
|
||||||
bind.mainSetLocalOption(
|
|
||||||
key: kOptionTextureRenderHealth, value: 'failed-raster-stall');
|
|
||||||
debugPrint(
|
|
||||||
'raster thread stall detected, texture rendering disabled for next launch');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -9,9 +9,6 @@ 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/consts.dart';
|
import 'package:flutter_hbb/consts.dart';
|
||||||
import 'package:flutter_hbb/utils/multi_window_manager.dart';
|
|
||||||
import 'package:flutter_hbb/plugin/widgets/desc_ui.dart';
|
|
||||||
import 'package:flutter_hbb/plugin/common.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:provider/provider.dart';
|
||||||
@@ -1336,6 +1333,12 @@ class ScreenAdjustor {
|
|||||||
final FFI ffi;
|
final FFI ffi;
|
||||||
final VoidCallback cbExitFullscreen;
|
final VoidCallback cbExitFullscreen;
|
||||||
window_size.Screen? _screen;
|
window_size.Screen? _screen;
|
||||||
|
Size? _waylandMaximizedWorkAreaSize;
|
||||||
|
Rect? _waylandWorkAreaScreenFrame;
|
||||||
|
double? _waylandWorkAreaScaleFactor;
|
||||||
|
Rect? _x11WorkArea;
|
||||||
|
Rect? _x11WorkAreaScreenFrame;
|
||||||
|
double? _x11WorkAreaScaleFactor;
|
||||||
|
|
||||||
ScreenAdjustor({
|
ScreenAdjustor({
|
||||||
required this.id,
|
required this.id,
|
||||||
@@ -1346,9 +1349,18 @@ class ScreenAdjustor {
|
|||||||
bool get isFullscreen => stateGlobal.fullscreen.isTrue;
|
bool get isFullscreen => stateGlobal.fullscreen.isTrue;
|
||||||
int get windowId => stateGlobal.windowId;
|
int get windowId => stateGlobal.windowId;
|
||||||
|
|
||||||
|
Future<bool?> isWindowMaximized() async {
|
||||||
|
try {
|
||||||
|
return await WindowController.fromWindowId(windowId).isMaximized();
|
||||||
|
} catch (_) {
|
||||||
|
// The delayed resolution callback may run after the window is disposed.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
adjustWindow(BuildContext context) {
|
adjustWindow(BuildContext context) {
|
||||||
return futureBuilder(
|
return futureBuilder(
|
||||||
future: isWindowCanBeAdjusted(),
|
future: isWindowCanBeAdjusted(context),
|
||||||
hasData: (data) {
|
hasData: (data) {
|
||||||
final visible = data as bool;
|
final visible = data as bool;
|
||||||
if (!visible) return Offstage();
|
if (!visible) return Offstage();
|
||||||
@@ -1364,36 +1376,201 @@ class ScreenAdjustor {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
doAdjustWindow(BuildContext context) async {
|
// Linux screen and work-area coordinates can use different units or become
|
||||||
await updateScreen();
|
// unreliable across Wayland/X11 state changes, so normalize reported frames
|
||||||
if (_screen != null) {
|
// and cache usable work-area measurements before sizing the window.
|
||||||
cbExitFullscreen();
|
|
||||||
double scale = _screen!.scaleFactor;
|
Future<void> _updateLinuxWorkAreaCache({
|
||||||
final wndRect = await WindowController.fromWindowId(windowId).getFrame();
|
required window_size.Screen screen,
|
||||||
final mediaSize = MediaQueryData.fromView(View.of(context)).size;
|
required Rect wndRect,
|
||||||
// On windows, wndRect is equal to GetWindowRect and mediaSize is equal to GetClientRect.
|
required bool isWayland,
|
||||||
|
required bool isX11,
|
||||||
|
required bool forMenu,
|
||||||
|
}) async {
|
||||||
|
if (isWayland &&
|
||||||
|
(_waylandWorkAreaScreenFrame != screen.frame ||
|
||||||
|
_waylandWorkAreaScaleFactor != screen.scaleFactor)) {
|
||||||
|
_waylandMaximizedWorkAreaSize = null;
|
||||||
|
_waylandWorkAreaScreenFrame = screen.frame;
|
||||||
|
_waylandWorkAreaScaleFactor = screen.scaleFactor;
|
||||||
|
}
|
||||||
|
if (isWayland &&
|
||||||
|
forMenu &&
|
||||||
|
!isFullscreen &&
|
||||||
|
await isWindowMaximized() == true) {
|
||||||
|
_waylandMaximizedWorkAreaSize = wndRect.size;
|
||||||
|
}
|
||||||
|
if (isX11 &&
|
||||||
|
(_x11WorkAreaScreenFrame != screen.frame ||
|
||||||
|
_x11WorkAreaScaleFactor != screen.scaleFactor)) {
|
||||||
|
_x11WorkArea = null;
|
||||||
|
_x11WorkAreaScreenFrame = screen.frame;
|
||||||
|
_x11WorkAreaScaleFactor = screen.scaleFactor;
|
||||||
|
}
|
||||||
|
if (isX11 && forMenu && !isFullscreen) {
|
||||||
|
_x11WorkArea = screen.visibleFrame;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Rect?> _getEffectiveScreenFrame({
|
||||||
|
required window_size.Screen screen,
|
||||||
|
required bool isWayland,
|
||||||
|
required bool isX11,
|
||||||
|
required bool forMenu,
|
||||||
|
}) async {
|
||||||
|
Rect frameRect = screen.visibleFrame;
|
||||||
|
if (isMacOS && forMenu && isFullscreen) {
|
||||||
|
List<double>? workArea;
|
||||||
|
try {
|
||||||
|
workArea = await kMacOSPermChannel
|
||||||
|
.invokeListMethod<double>('getMacOSWorkAreaSize');
|
||||||
|
} catch (_) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (workArea == null || workArea.length != 2) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
frameRect = Rect.fromLTWH(
|
||||||
|
frameRect.left,
|
||||||
|
frameRect.top,
|
||||||
|
workArea[0] < frameRect.width ? workArea[0] : frameRect.width,
|
||||||
|
workArea[1] < frameRect.height ? workArea[1] : frameRect.height,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
final x11WorkArea = _x11WorkArea;
|
||||||
|
if (isX11 &&
|
||||||
|
forMenu &&
|
||||||
|
isFullscreen &&
|
||||||
|
x11WorkArea != null &&
|
||||||
|
(x11WorkArea.width < frameRect.width ||
|
||||||
|
x11WorkArea.height < frameRect.height)) {
|
||||||
|
frameRect = x11WorkArea;
|
||||||
|
}
|
||||||
|
final screenScale = screen.scaleFactor;
|
||||||
|
if (isWayland && screenScale > 1.01) {
|
||||||
|
String monitorLayoutMode;
|
||||||
|
try {
|
||||||
|
monitorLayoutMode =
|
||||||
|
await bind.mainGetCommon(key: 'gnome-monitor-layout-mode');
|
||||||
|
} catch (_) {
|
||||||
|
monitorLayoutMode = '';
|
||||||
|
}
|
||||||
|
if (monitorLayoutMode == 'physical') {
|
||||||
|
frameRect = Rect.fromLTRB(
|
||||||
|
frameRect.left / screenScale,
|
||||||
|
frameRect.top / screenScale,
|
||||||
|
frameRect.right / screenScale,
|
||||||
|
frameRect.bottom / screenScale,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return frameRect;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Rect?> _getAdjustedWindowFrame(Size mediaSize,
|
||||||
|
{bool forMenu = false}) async {
|
||||||
|
final screen = _screen;
|
||||||
|
if (screen != null) {
|
||||||
|
// Windows window frames use physical pixels while Flutter view sizes are
|
||||||
|
// logical. macOS and Linux window frames use the same units as Flutter.
|
||||||
|
double scale = isWindows ? screen.scaleFactor : 1.0;
|
||||||
|
final Rect wndRect;
|
||||||
|
try {
|
||||||
|
wndRect = await WindowController.fromWindowId(windowId).getFrame();
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint("Failed to get frame of window $windowId, it may be hidden");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// On Windows, wndRect is GetWindowRect while mediaSize is GetClientRect.
|
||||||
// https://stackoverflow.com/a/7561083
|
// https://stackoverflow.com/a/7561083
|
||||||
double magicWidth =
|
double magicWidth =
|
||||||
wndRect.right - wndRect.left - mediaSize.width * scale;
|
wndRect.right - wndRect.left - mediaSize.width * scale;
|
||||||
double magicHeight =
|
double magicHeight =
|
||||||
wndRect.bottom - wndRect.top - mediaSize.height * scale;
|
wndRect.bottom - wndRect.top - mediaSize.height * scale;
|
||||||
final canvasModel = ffi.canvasModel;
|
final canvasModel = ffi.canvasModel;
|
||||||
|
// canvasModel.scale is the rendered scale and already applies kIgnoreDpi.
|
||||||
|
// Use it instead of the remote source resolution.
|
||||||
|
final isWayland = isLinux && bind.mainCurrentIsWayland();
|
||||||
|
final isX11 = isLinux && !isWayland;
|
||||||
|
await _updateLinuxWorkAreaCache(
|
||||||
|
screen: screen,
|
||||||
|
wndRect: wndRect,
|
||||||
|
isWayland: isWayland,
|
||||||
|
isX11: isX11,
|
||||||
|
forMenu: forMenu,
|
||||||
|
);
|
||||||
|
if (isWindows && forMenu && isFullscreen) {
|
||||||
|
// desktop_multi_window's hidden title bar keeps 8 physical pixels on
|
||||||
|
// each horizontal edge and at the bottom, plus up to 1px at the top.
|
||||||
|
// Fullscreen removes these in WM_NCCALCSIZE, so predict the restored
|
||||||
|
// frame's worst-case padding when deciding whether to show the menu.
|
||||||
|
magicWidth = 16.0;
|
||||||
|
magicHeight = 9.0;
|
||||||
|
}
|
||||||
|
double horizontalEdges;
|
||||||
|
double verticalEdges;
|
||||||
|
if (forMenu && (isLinux || ((isMacOS || isWindows) && isFullscreen))) {
|
||||||
|
// Linux Adjust Window unmaximizes; macOS and Windows exit fullscreen
|
||||||
|
// before resizing. Predict the restored normal-window edges when
|
||||||
|
// deciding whether to show the menu item.
|
||||||
|
final resizePadding = isLinux && !kUseCompatibleUiMode
|
||||||
|
? kDragToResizeAreaPaddingSize
|
||||||
|
: 0.0;
|
||||||
|
final windowEdge = kWindowBorderWidth + resizePadding;
|
||||||
|
horizontalEdges = windowEdge * 2;
|
||||||
|
verticalEdges = kDesktopRemoteTabBarHeight + windowEdge * 2;
|
||||||
|
} else {
|
||||||
|
horizontalEdges = CanvasModel.leftToEdge + CanvasModel.rightToEdge;
|
||||||
|
verticalEdges = CanvasModel.topToEdge + CanvasModel.bottomToEdge;
|
||||||
|
}
|
||||||
final width = (canvasModel.getDisplayWidth() * canvasModel.scale +
|
final width = (canvasModel.getDisplayWidth() * canvasModel.scale +
|
||||||
CanvasModel.leftToEdge +
|
horizontalEdges) *
|
||||||
CanvasModel.rightToEdge) *
|
|
||||||
scale +
|
scale +
|
||||||
magicWidth;
|
magicWidth;
|
||||||
final height = (canvasModel.getDisplayHeight() * canvasModel.scale +
|
final height =
|
||||||
CanvasModel.topToEdge +
|
(canvasModel.getDisplayHeight() * canvasModel.scale + verticalEdges) *
|
||||||
CanvasModel.bottomToEdge) *
|
scale +
|
||||||
scale +
|
magicHeight;
|
||||||
magicHeight;
|
|
||||||
double left = wndRect.left + (wndRect.width - width) / 2;
|
double left = wndRect.left + (wndRect.width - width) / 2;
|
||||||
double top = wndRect.top + (wndRect.height - height) / 2;
|
double top = wndRect.top + (wndRect.height - height) / 2;
|
||||||
|
|
||||||
Rect frameRect = _screen!.frame;
|
final frameRect = await _getEffectiveScreenFrame(
|
||||||
if (!isFullscreen) {
|
screen: screen,
|
||||||
frameRect = _screen!.visibleFrame;
|
isWayland: isWayland,
|
||||||
|
isX11: isX11,
|
||||||
|
forMenu: forMenu,
|
||||||
|
);
|
||||||
|
if (frameRect == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
var availableSize = frameRect.size;
|
||||||
|
if (isWayland && forMenu && _waylandMaximizedWorkAreaSize != null) {
|
||||||
|
final cachedSize = _waylandMaximizedWorkAreaSize!;
|
||||||
|
availableSize = Size(
|
||||||
|
cachedSize.width < availableSize.width
|
||||||
|
? cachedSize.width
|
||||||
|
: availableSize.width,
|
||||||
|
cachedSize.height < availableSize.height
|
||||||
|
? cachedSize.height
|
||||||
|
: availableSize.height,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// A window frame cannot be smaller than its client area. Tolerate small
|
||||||
|
// floating-point differences; larger negative values mean the native
|
||||||
|
// frame and Flutter view metrics are not synchronized.
|
||||||
|
if (magicWidth < -0.1 || magicHeight < -0.1) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// Reject implausibly small targets to avoid hiding the window.
|
||||||
|
if (width < 300 || height < 300) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// The remote size may change after the menu is built. Reject targets
|
||||||
|
// that exceed the available area.
|
||||||
|
final exceedsScreen =
|
||||||
|
width > availableSize.width || height > availableSize.height;
|
||||||
|
if (exceedsScreen) {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
if (left < frameRect.left) {
|
if (left < frameRect.left) {
|
||||||
left = frameRect.left;
|
left = frameRect.left;
|
||||||
@@ -1407,69 +1584,101 @@ class ScreenAdjustor {
|
|||||||
if ((top + height) > frameRect.bottom) {
|
if ((top + height) > frameRect.bottom) {
|
||||||
top = frameRect.bottom - height;
|
top = frameRect.bottom - height;
|
||||||
}
|
}
|
||||||
await WindowController.fromWindowId(windowId)
|
return Rect.fromLTWH(left, top, width, height);
|
||||||
.setFrame(Rect.fromLTWH(left, top, width, height));
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
doAdjustWindow([BuildContext? context]) async {
|
||||||
|
// A resolution change is adjusted after a delay, when the menu context may
|
||||||
|
// already be disposed. Each desktop_multi_window window has its own engine,
|
||||||
|
// so that engine's first view is the current window.
|
||||||
|
final views = WidgetsBinding.instance.platformDispatcher.views;
|
||||||
|
if (context == null && views.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final view = context != null ? View.of(context) : views.first;
|
||||||
|
await updateScreen();
|
||||||
|
if (_screen != null) {
|
||||||
|
final wc = WindowController.fromWindowId(windowId);
|
||||||
|
final wasFullscreen = isFullscreen;
|
||||||
|
cbExitFullscreen();
|
||||||
|
if (wasFullscreen) {
|
||||||
|
// Wait for the native fullscreen exit to update the window frame.
|
||||||
|
await Future.delayed(Duration(milliseconds: 700));
|
||||||
|
await updateScreen();
|
||||||
|
}
|
||||||
|
if (isLinux) {
|
||||||
|
final isMaximized = await isWindowMaximized();
|
||||||
|
if (isMaximized == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isMaximized == true) {
|
||||||
|
// setFrame may be ignored while the native window is maximized.
|
||||||
|
try {
|
||||||
|
await wc.unmaximize();
|
||||||
|
} catch (_) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
stateGlobal.setMaximized(false);
|
||||||
|
// Wait for the window manager and Flutter view metrics to reflect
|
||||||
|
// the restored window before calculating and setting its frame.
|
||||||
|
await Future.delayed(Duration(milliseconds: 300));
|
||||||
|
await updateScreen();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final mediaSize = MediaQueryData.fromView(view).size;
|
||||||
|
final frame = await _getAdjustedWindowFrame(mediaSize);
|
||||||
|
if (frame == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await wc.setFrame(frame);
|
||||||
|
} catch (_) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
stateGlobal.setMaximized(false);
|
stateGlobal.setMaximized(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateScreen() async {
|
updateScreen() async {
|
||||||
final String info =
|
_screen = await _getCurrentScreen();
|
||||||
isWeb ? screenInfo : await _getScreenInfoDesktop() ?? '';
|
}
|
||||||
if (info.isEmpty) {
|
|
||||||
_screen = null;
|
Future<window_size.Screen?> _getCurrentScreen() async {
|
||||||
} else {
|
try {
|
||||||
final screenMap = jsonDecode(info);
|
return (await window_size.getWindowInfo()).screen;
|
||||||
_screen = window_size.Screen(
|
} catch (e) {
|
||||||
Rect.fromLTRB(screenMap['frame']['l'], screenMap['frame']['t'],
|
debugPrint('Failed to get current window screen: $e');
|
||||||
screenMap['frame']['r'], screenMap['frame']['b']),
|
return null;
|
||||||
Rect.fromLTRB(
|
|
||||||
screenMap['visibleFrame']['l'],
|
|
||||||
screenMap['visibleFrame']['t'],
|
|
||||||
screenMap['visibleFrame']['r'],
|
|
||||||
screenMap['visibleFrame']['b']),
|
|
||||||
screenMap['scaleFactor']);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_getScreenInfoDesktop() async {
|
Future<bool> isWindowCanBeAdjusted([BuildContext? context]) async {
|
||||||
final v = await rustDeskWinManager.call(
|
if (isWeb) {
|
||||||
WindowType.Main, kWindowGetWindowInfo, '');
|
return false;
|
||||||
return v.result;
|
}
|
||||||
}
|
// Capture the view before awaiting because the menu context may be disposed.
|
||||||
|
final views = WidgetsBinding.instance.platformDispatcher.views;
|
||||||
Future<bool> isWindowCanBeAdjusted() async {
|
if (context == null && views.isEmpty) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
final view = context != null ? View.of(context) : views.first;
|
||||||
|
final mediaSize = MediaQueryData.fromView(view).size;
|
||||||
final viewStyle =
|
final viewStyle =
|
||||||
await bind.sessionGetViewStyle(sessionId: ffi.sessionId) ?? '';
|
await bind.sessionGetViewStyle(sessionId: ffi.sessionId) ?? '';
|
||||||
if (viewStyle != kRemoteViewStyleOriginal) {
|
if (viewStyle != kRemoteViewStyleOriginal) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!isWeb) {
|
final remoteCount = RemoteCountState.find().value;
|
||||||
final remoteCount = RemoteCountState.find().value;
|
if (remoteCount != 1) {
|
||||||
if (remoteCount != 1) {
|
return false;
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
await updateScreen();
|
||||||
if (_screen == null) {
|
if (_screen == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
final scale = kIgnoreDpi ? 1.0 : _screen!.scaleFactor;
|
return await _getAdjustedWindowFrame(mediaSize, forMenu: true) != null;
|
||||||
double selfWidth = _screen!.visibleFrame.width;
|
|
||||||
double selfHeight = _screen!.visibleFrame.height;
|
|
||||||
if (isFullscreen) {
|
|
||||||
selfWidth = _screen!.frame.width;
|
|
||||||
selfHeight = _screen!.frame.height;
|
|
||||||
}
|
|
||||||
|
|
||||||
final canvasModel = ffi.canvasModel;
|
|
||||||
final displayWidth = canvasModel.getDisplayWidth();
|
|
||||||
final displayHeight = canvasModel.getDisplayHeight();
|
|
||||||
final requiredWidth =
|
|
||||||
CanvasModel.leftToEdge + displayWidth + CanvasModel.rightToEdge;
|
|
||||||
final requiredHeight =
|
|
||||||
CanvasModel.topToEdge + displayHeight + CanvasModel.bottomToEdge;
|
|
||||||
return selfWidth > (requiredWidth * scale) &&
|
|
||||||
selfHeight > (requiredHeight * scale);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1478,20 +1687,11 @@ class _DisplayMenu extends StatefulWidget {
|
|||||||
final FFI ffi;
|
final FFI ffi;
|
||||||
final ToolbarState state;
|
final ToolbarState state;
|
||||||
final Function(bool) setFullscreen;
|
final Function(bool) setFullscreen;
|
||||||
final Widget pluginItem;
|
const _DisplayMenu(
|
||||||
_DisplayMenu(
|
{required this.id,
|
||||||
{Key? key,
|
|
||||||
required this.id,
|
|
||||||
required this.ffi,
|
required this.ffi,
|
||||||
required this.state,
|
required this.state,
|
||||||
required this.setFullscreen})
|
required this.setFullscreen});
|
||||||
: pluginItem = LocationItem.createLocationItem(
|
|
||||||
id,
|
|
||||||
ffi,
|
|
||||||
kLocationClientRemoteToolbarDisplay,
|
|
||||||
true,
|
|
||||||
),
|
|
||||||
super(key: key);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<_DisplayMenu> createState() => _DisplayMenuState();
|
State<_DisplayMenu> createState() => _DisplayMenuState();
|
||||||
@@ -1529,7 +1729,6 @@ class _DisplayMenuState extends State<_DisplayMenu> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final colorScheme = Theme.of(context).colorScheme;
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
_screenAdjustor.updateScreen();
|
|
||||||
menuChildrenGetter(_IconSubmenuButtonState state) {
|
menuChildrenGetter(_IconSubmenuButtonState state) {
|
||||||
final menuChildren = <Widget>[
|
final menuChildren = <Widget>[
|
||||||
_screenAdjustor.adjustWindow(context),
|
_screenAdjustor.adjustWindow(context),
|
||||||
@@ -1582,9 +1781,6 @@ class _DisplayMenuState extends State<_DisplayMenu> {
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (ffi.connType == ConnType.defaultConn) {
|
|
||||||
menuChildren.add(widget.pluginItem);
|
|
||||||
}
|
|
||||||
return menuChildren;
|
return menuChildren;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2096,15 +2292,19 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> {
|
|||||||
|
|
||||||
Future<void> _getLocalResolutionWayland() async {
|
Future<void> _getLocalResolutionWayland() async {
|
||||||
if (!isWayland) return _getLocalResolution();
|
if (!isWayland) return _getLocalResolution();
|
||||||
final window = await window_size.getWindowInfo();
|
try {
|
||||||
final screen = window.screen;
|
final window = await window_size.getWindowInfo();
|
||||||
if (screen != null) {
|
final screen = window.screen;
|
||||||
setState(() {
|
if (screen != null) {
|
||||||
_localResolution = Resolution(
|
setState(() {
|
||||||
screen.frame.width.toInt(),
|
_localResolution = Resolution(
|
||||||
screen.frame.height.toInt(),
|
screen.frame.width.toInt(),
|
||||||
);
|
screen.frame.height.toInt(),
|
||||||
});
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('Failed to get local resolution on Wayland: $e');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2176,8 +2376,16 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (w == rect.width.toInt() && h == rect.height.toInt()) {
|
if (w == rect.width.toInt() && h == rect.height.toInt()) {
|
||||||
if (await widget.screenAdjustor.isWindowCanBeAdjusted()) {
|
if (!await widget.screenAdjustor.isWindowCanBeAdjusted()) {
|
||||||
widget.screenAdjustor.doAdjustWindow(context);
|
return;
|
||||||
|
}
|
||||||
|
if (widget.screenAdjustor.isFullscreen) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ((await widget.screenAdjustor.isWindowMaximized()) == false) {
|
||||||
|
// This delayed callback can outlive the menu State, so its context
|
||||||
|
// is unsafe.
|
||||||
|
widget.screenAdjustor.doAdjustWindow();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import 'package:flutter_hbb/consts.dart';
|
|||||||
import 'package:flutter_hbb/desktop/pages/remote_page.dart';
|
import 'package:flutter_hbb/desktop/pages/remote_page.dart';
|
||||||
import 'package:flutter_hbb/desktop/pages/view_camera_page.dart';
|
import 'package:flutter_hbb/desktop/pages/view_camera_page.dart';
|
||||||
import 'package:flutter_hbb/main.dart';
|
import 'package:flutter_hbb/main.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/state_model.dart';
|
import 'package:flutter_hbb/models/state_model.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
@@ -389,7 +388,6 @@ class _DesktopTabState extends State<DesktopTab>
|
|||||||
void onWindowMinimize() {
|
void onWindowMinimize() {
|
||||||
stateGlobal.setMinimized(true);
|
stateGlobal.setMinimized(true);
|
||||||
stateGlobal.setMaximized(false);
|
stateGlobal.setMaximized(false);
|
||||||
_updateSessionsRenderVisible(false);
|
|
||||||
super.onWindowMinimize();
|
super.onWindowMinimize();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -397,7 +395,6 @@ class _DesktopTabState extends State<DesktopTab>
|
|||||||
void onWindowMaximize() {
|
void onWindowMaximize() {
|
||||||
stateGlobal.setMinimized(false);
|
stateGlobal.setMinimized(false);
|
||||||
_setMaximized(true);
|
_setMaximized(true);
|
||||||
_updateSessionsRenderVisible(true);
|
|
||||||
super.onWindowMaximize();
|
super.onWindowMaximize();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -405,34 +402,9 @@ class _DesktopTabState extends State<DesktopTab>
|
|||||||
void onWindowUnmaximize() {
|
void onWindowUnmaximize() {
|
||||||
stateGlobal.setMinimized(false);
|
stateGlobal.setMinimized(false);
|
||||||
_setMaximized(false);
|
_setMaximized(false);
|
||||||
_updateSessionsRenderVisible(true);
|
|
||||||
super.onWindowUnmaximize();
|
super.onWindowUnmaximize();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
void onWindowRestore() {
|
|
||||||
// A plain restore (no maximize involved) must clear the minimized flag.
|
|
||||||
stateGlobal.setMinimized(false);
|
|
||||||
_updateSessionsRenderVisible(true);
|
|
||||||
super.onWindowRestore();
|
|
||||||
}
|
|
||||||
|
|
||||||
// A hidden window composites nothing; pause the Rust-side texture watchdog
|
|
||||||
// for its sessions so it cannot record a false failure.
|
|
||||||
void _updateSessionsRenderVisible(bool visible) {
|
|
||||||
if (tabType != DesktopTabType.remoteScreen &&
|
|
||||||
tabType != DesktopTabType.viewCamera) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for (final tab in controller.state.value.tabs) {
|
|
||||||
try {
|
|
||||||
final ffi = Get.find<FFI>(tag: tab.key);
|
|
||||||
bind.sessionSetRenderVisible(
|
|
||||||
sessionId: ffi.sessionId, visible: visible);
|
|
||||||
} catch (_) {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_saveFrame({bool? flush}) async {
|
_saveFrame({bool? flush}) async {
|
||||||
try {
|
try {
|
||||||
if (tabType == DesktopTabType.main) {
|
if (tabType == DesktopTabType.main) {
|
||||||
|
|||||||
@@ -1,172 +0,0 @@
|
|||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter/scheduler.dart';
|
|
||||||
|
|
||||||
import '../../common.dart';
|
|
||||||
import '../../consts.dart';
|
|
||||||
import '../../models/platform_model.dart';
|
|
||||||
import '../../models/state_model.dart';
|
|
||||||
|
|
||||||
import 'package:texture_rgba_renderer/texture_rgba_renderer.dart'
|
|
||||||
if (dart.library.html) 'package:flutter_hbb/web/texture_rgba_renderer.dart';
|
|
||||||
|
|
||||||
/// Startup probe: renders one frame through a 1x1 external texture and
|
|
||||||
/// records in `texture-render-health` whether the engine consumed it, so a
|
|
||||||
/// broken environment is detected before the first session goes black.
|
|
||||||
class TextureRenderProbe extends StatefulWidget {
|
|
||||||
const TextureRenderProbe({Key? key}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<TextureRenderProbe> createState() => _TextureRenderProbeState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _TextureRenderProbeState extends State<TextureRenderProbe> {
|
|
||||||
static bool _ranThisLaunch = false;
|
|
||||||
final _renderer = TextureRgbaRenderer();
|
|
||||||
int _textureId = -1;
|
|
||||||
int _textureKey = -1;
|
|
||||||
int _ptr = 0;
|
|
||||||
Timer? _timer;
|
|
||||||
int _ticks = 0;
|
|
||||||
bool _sawTimings = false;
|
|
||||||
bool _wasEffectiveOn = false;
|
|
||||||
DateTime? _lastTimings;
|
|
||||||
DateTime? _firstPush;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
if (_ranThisLaunch || isWeb || !isDesktop) return;
|
|
||||||
_ranThisLaunch = true;
|
|
||||||
if (bind.isIncomingOnly()) return;
|
|
||||||
// An old plugin without the consumed counter cannot be judged, and a
|
|
||||||
// recorded raster-stall means compositing a texture may hang this window.
|
|
||||||
if (!bind.mainTextureRenderProbeSupported()) return;
|
|
||||||
if (bind
|
|
||||||
.mainGetLocalOption(key: kOptionTextureRenderHealth)
|
|
||||||
.startsWith('failed-raster-stall')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
_wasEffectiveOn = bind.mainGetUseTextureRender();
|
|
||||||
// Only probe after the window has really rendered a frame: a hidden
|
|
||||||
// window (silent/tray start) must not record a false failure.
|
|
||||||
SchedulerBinding.instance.addTimingsCallback(_onTimings);
|
|
||||||
Future.delayed(const Duration(seconds: 5), () {
|
|
||||||
if (!_sawTimings) {
|
|
||||||
SchedulerBinding.instance.removeTimingsCallback(_onTimings);
|
|
||||||
_finish(null);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void _onTimings(List<FrameTiming> timings) {
|
|
||||||
_lastTimings = DateTime.now();
|
|
||||||
if (_sawTimings) return;
|
|
||||||
_sawTimings = true;
|
|
||||||
_start();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _start() async {
|
|
||||||
if (!mounted) return;
|
|
||||||
_textureKey = bind.getNextTextureKey();
|
|
||||||
final id = await _renderer.createTexture(_textureKey);
|
|
||||||
if (!mounted || id == -1) {
|
|
||||||
_finish(!mounted ? null : false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
_ptr = await _renderer.getTexturePtr(_textureKey);
|
|
||||||
if (!mounted || _ptr <= 0) {
|
|
||||||
_finish(!mounted ? null : false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setState(() => _textureId = id);
|
|
||||||
_timer = Timer.periodic(const Duration(milliseconds: 100), (_) {
|
|
||||||
_ticks += 1;
|
|
||||||
_firstPush ??= DateTime.now();
|
|
||||||
bind.mainPushTextureProbeFrame(ptr: _ptr);
|
|
||||||
final consumed = bind.mainGetTextureProbeConsumed(ptr: _ptr) > 0;
|
|
||||||
// "Consumed" advances inside the plugin callback, before the GL/Metal
|
|
||||||
// upload; only a frame timing after the push proves a completed frame.
|
|
||||||
final frameCompleted = consumed &&
|
|
||||||
_lastTimings != null &&
|
|
||||||
_lastTimings!.isAfter(_firstPush!);
|
|
||||||
if (frameCompleted) {
|
|
||||||
_finish(true);
|
|
||||||
} else if (_ticks >= 10) {
|
|
||||||
if (consumed) {
|
|
||||||
_finish(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Only a window that is visibly compositing can prove a failure.
|
|
||||||
final lifecycle = SchedulerBinding.instance.lifecycleState;
|
|
||||||
final active =
|
|
||||||
lifecycle == null || lifecycle == AppLifecycleState.resumed;
|
|
||||||
final timingsFresh = _lastTimings != null &&
|
|
||||||
DateTime.now().difference(_lastTimings!) <
|
|
||||||
const Duration(milliseconds: 1500);
|
|
||||||
_finish(!stateGlobal.isMinimized && active && timingsFresh
|
|
||||||
? false
|
|
||||||
: null);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void _finish(bool? ok) {
|
|
||||||
_timer?.cancel();
|
|
||||||
_timer = null;
|
|
||||||
SchedulerBinding.instance.removeTimingsCallback(_onTimings);
|
|
||||||
if (ok != null) {
|
|
||||||
final old = bind.mainGetLocalOption(key: kOptionTextureRenderHealth);
|
|
||||||
if (ok) {
|
|
||||||
// This rgba probe disproves only the rgba black-texture class: gpu
|
|
||||||
// failures and raster stalls clear via the option toggle alone.
|
|
||||||
final clearable = old.isEmpty ||
|
|
||||||
old.startsWith('failed-probe') ||
|
|
||||||
old.startsWith('failed-watchdog-rgba');
|
|
||||||
if (clearable) {
|
|
||||||
bind.mainSetLocalOption(key: kOptionTextureRenderHealth, value: 'ok');
|
|
||||||
}
|
|
||||||
} else if (!old.startsWith('failed')) {
|
|
||||||
debugPrint('texture render probe failed, disabling texture rendering');
|
|
||||||
bind.mainSetLocalOption(
|
|
||||||
key: kOptionTextureRenderHealth, value: 'failed-probe');
|
|
||||||
if (_wasEffectiveOn) {
|
|
||||||
showToast(translate('texture-render-fallback-tip'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (_textureKey != -1) {
|
|
||||||
_renderer.closeTexture(_textureKey);
|
|
||||||
_textureKey = -1;
|
|
||||||
}
|
|
||||||
_ptr = 0;
|
|
||||||
if (mounted && _textureId != -1) {
|
|
||||||
setState(() => _textureId = -1);
|
|
||||||
} else {
|
|
||||||
_textureId = -1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_timer?.cancel();
|
|
||||||
SchedulerBinding.instance.removeTimingsCallback(_onTimings);
|
|
||||||
if (_textureKey != -1) {
|
|
||||||
_renderer.closeTexture(_textureKey);
|
|
||||||
_textureKey = -1;
|
|
||||||
}
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
if (_textureId == -1) return const SizedBox.shrink();
|
|
||||||
// Must actually composite for the engine to sample the texture; the
|
|
||||||
// pushed pixel is fully transparent.
|
|
||||||
return IgnorePointer(
|
|
||||||
child: SizedBox(
|
|
||||||
width: 1, height: 1, child: Texture(textureId: _textureId)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -30,9 +30,6 @@ import 'mobile/pages/server_page.dart';
|
|||||||
import 'mobile/widgets/deploy_dialog.dart';
|
import 'mobile/widgets/deploy_dialog.dart';
|
||||||
import 'models/platform_model.dart';
|
import 'models/platform_model.dart';
|
||||||
|
|
||||||
import 'package:flutter_hbb/plugin/handlers.dart'
|
|
||||||
if (dart.library.html) 'package:flutter_hbb/web/plugin/handlers.dart';
|
|
||||||
|
|
||||||
/// Basic window and launch properties.
|
/// Basic window and launch properties.
|
||||||
int? kWindowId;
|
int? kWindowId;
|
||||||
WindowType? kWindowType;
|
WindowType? kWindowType;
|
||||||
@@ -141,8 +138,6 @@ void runMainApp(bool startService) async {
|
|||||||
await bind.mainCheckConnectStatus();
|
await bind.mainCheckConnectStatus();
|
||||||
if (startService) {
|
if (startService) {
|
||||||
gFFI.serverModel.startService();
|
gFFI.serverModel.startService();
|
||||||
bind.pluginSyncUi(syncTo: kAppTypeMain);
|
|
||||||
bind.pluginListReload();
|
|
||||||
}
|
}
|
||||||
await Future.wait([gFFI.abModel.loadCache(), gFFI.groupModel.loadCache()]);
|
await Future.wait([gFFI.abModel.loadCache(), gFFI.groupModel.loadCache()]);
|
||||||
gFFI.userModel.refreshCurrentUser();
|
gFFI.userModel.refreshCurrentUser();
|
||||||
@@ -570,12 +565,6 @@ _registerEventHandler() {
|
|||||||
reloadAllWindows();
|
reloadAllWindows();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// Register native handlers.
|
|
||||||
if (isDesktop) {
|
|
||||||
platformFFI.registerEventHandler('native_ui', 'native_ui', (evt) async {
|
|
||||||
NativeUiHandler.instance.onEvent(evt);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (isAndroid) {
|
if (isAndroid) {
|
||||||
platformFFI.registerEventHandler(
|
platformFFI.registerEventHandler(
|
||||||
'android_needs_deploy', 'android_needs_deploy', (_) async {
|
'android_needs_deploy', 'android_needs_deploy', (_) async {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_breadcrumb/flutter_breadcrumb.dart';
|
import 'package:flutter_breadcrumb/flutter_breadcrumb.dart';
|
||||||
@@ -8,6 +9,7 @@ import 'package:toggle_switch/toggle_switch.dart';
|
|||||||
|
|
||||||
import '../../common.dart';
|
import '../../common.dart';
|
||||||
import '../../common/widgets/dialog.dart';
|
import '../../common/widgets/dialog.dart';
|
||||||
|
import '../../consts.dart';
|
||||||
|
|
||||||
class FileManagerPage extends StatefulWidget {
|
class FileManagerPage extends StatefulWidget {
|
||||||
FileManagerPage(
|
FileManagerPage(
|
||||||
@@ -73,6 +75,173 @@ class _FileManagerPageState extends State<FileManagerPage> {
|
|||||||
DirectoryOptions get currentOptions => currentFileController.options.value;
|
DirectoryOptions get currentOptions => currentFileController.options.value;
|
||||||
final _uniqueKey = UniqueKey();
|
final _uniqueKey = UniqueKey();
|
||||||
|
|
||||||
|
Future<T> _runAndroidDocumentPicker<T>(Future<T> Function() action) async {
|
||||||
|
gFFI.ffiModel.beginAndroidDocumentPicker();
|
||||||
|
try {
|
||||||
|
return await action();
|
||||||
|
} finally {
|
||||||
|
gFFI.ffiModel.endAndroidDocumentPicker();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _importFiles() async {
|
||||||
|
var imported = 0;
|
||||||
|
var failed = false;
|
||||||
|
final importController = currentFileController;
|
||||||
|
final importDirectory = currentDir.path;
|
||||||
|
final importIsWindows = currentOptions.isWindows;
|
||||||
|
try {
|
||||||
|
final selectedFiles = await _runAndroidDocumentPicker(() =>
|
||||||
|
gFFI.invokeMethodWithResult<List<dynamic>>(
|
||||||
|
AndroidChannel.kPickImportFiles));
|
||||||
|
if (selectedFiles == null || selectedFiles.isEmpty) return;
|
||||||
|
|
||||||
|
for (final selected in selectedFiles) {
|
||||||
|
final uri = (selected as Map<dynamic, dynamic>)['uri'] as String?;
|
||||||
|
final selectedName = selected['name'] as String?;
|
||||||
|
final name = selectedName?.replaceAll('\\', '/').split('/').last;
|
||||||
|
if (uri == null ||
|
||||||
|
name == null ||
|
||||||
|
!PathUtil.validName(name, importIsWindows)) {
|
||||||
|
failed = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final destination =
|
||||||
|
PathUtil.join(importDirectory, name, importIsWindows);
|
||||||
|
var overwrite = false;
|
||||||
|
if (await File(destination).exists()) {
|
||||||
|
final overwriteResult = await model.showFileConfirmDialog(
|
||||||
|
translate('Overwrite'), destination, false, false);
|
||||||
|
if (overwriteResult == false) break;
|
||||||
|
if (overwriteResult != true) continue;
|
||||||
|
overwrite = true;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
final success = await gFFI.invokeMethod(
|
||||||
|
AndroidChannel.kImportFile,
|
||||||
|
{'uri': uri, 'path': destination, 'overwrite': overwrite});
|
||||||
|
if (success == true) {
|
||||||
|
imported++;
|
||||||
|
} else {
|
||||||
|
failed = true;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
failed = true;
|
||||||
|
debugPrint('Failed to import $name: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
failed = true;
|
||||||
|
debugPrint('Failed to select files for import: $e');
|
||||||
|
}
|
||||||
|
await importController.refresh();
|
||||||
|
if (failed) {
|
||||||
|
showToast(translate('Failed'));
|
||||||
|
} else if (imported > 0) {
|
||||||
|
showToast(translate('Successful'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _exportFile(Entry entry) async {
|
||||||
|
try {
|
||||||
|
final exported = await _runAndroidDocumentPicker(() => gFFI
|
||||||
|
.invokeMethod(AndroidChannel.kExportFile, {'path': entry.path}));
|
||||||
|
if (exported == true) {
|
||||||
|
showToast(translate('Successful'));
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('Failed to export ${entry.name}: $e');
|
||||||
|
showToast(translate('Failed'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _importFolder() async {
|
||||||
|
final importController = currentFileController;
|
||||||
|
final importDirectory = currentDir.path;
|
||||||
|
final importIsWindows = currentOptions.isWindows;
|
||||||
|
try {
|
||||||
|
final picked = await _runAndroidDocumentPicker(() =>
|
||||||
|
gFFI.invokeMethodWithResult<Map<dynamic, dynamic>>(
|
||||||
|
AndroidChannel.kPickImportDirectory));
|
||||||
|
if (picked == null || picked.isEmpty) return;
|
||||||
|
final uri = picked['uri'] as String?;
|
||||||
|
final name =
|
||||||
|
(picked['name'] as String?)?.replaceAll('\\', '/').split('/').last;
|
||||||
|
if (uri == null ||
|
||||||
|
name == null ||
|
||||||
|
name == '.' ||
|
||||||
|
name == '..' ||
|
||||||
|
!PathUtil.validName(name, importIsWindows)) {
|
||||||
|
showToast(translate('Failed'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final destination = PathUtil.join(importDirectory, name, importIsWindows);
|
||||||
|
final destinationType = await FileSystemEntity.type(destination);
|
||||||
|
var overwrite = false;
|
||||||
|
if (destinationType == FileSystemEntityType.directory) {
|
||||||
|
final overwriteResult = await model.showFileConfirmDialog(
|
||||||
|
translate('Overwrite'), destination, false, false);
|
||||||
|
if (overwriteResult != true) return;
|
||||||
|
overwrite = true;
|
||||||
|
} else if (destinationType != FileSystemEntityType.notFound) {
|
||||||
|
showToast(translate('Failed'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final success = await gFFI.invokeMethod(AndroidChannel.kImportDirectory,
|
||||||
|
{'uri': uri, 'path': destination, 'overwrite': overwrite});
|
||||||
|
if (success == true) {
|
||||||
|
showToast(translate('Successful'));
|
||||||
|
} else {
|
||||||
|
showToast(translate('Failed'));
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('Failed to import folder: $e');
|
||||||
|
showToast(translate('Failed'));
|
||||||
|
}
|
||||||
|
await importController.refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _exportItems(SelectedItems items) async {
|
||||||
|
await _exportPaths(items.items.map((e) => e.path));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _exportLogs() async {
|
||||||
|
final home = currentFileController.homePath;
|
||||||
|
if (home.isEmpty) {
|
||||||
|
showToast(translate('Failed'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final appDir = PathUtil.join(home, appName, false);
|
||||||
|
final paths = [
|
||||||
|
PathUtil.join(appDir, 'Logs', false),
|
||||||
|
PathUtil.join(appDir, 'ScreenRecord', false),
|
||||||
|
].where((p) => File(p).existsSync() || Directory(p).existsSync()).toList();
|
||||||
|
if (paths.isEmpty) {
|
||||||
|
showToast(translate('Failed'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await _exportPaths(paths);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _exportPaths(Iterable<String> paths) async {
|
||||||
|
try {
|
||||||
|
final result = await _runAndroidDocumentPicker(() =>
|
||||||
|
gFFI.invokeMethodWithResult<Map<dynamic, dynamic>>(
|
||||||
|
AndroidChannel.kExportFiles, {'paths': paths.toList()}));
|
||||||
|
if (result == null) return;
|
||||||
|
final exported = result['exported'] as int? ?? 0;
|
||||||
|
final failed = result['failed'] as int? ?? 0;
|
||||||
|
if (failed > 0) {
|
||||||
|
showToast(translate('Failed'));
|
||||||
|
} else if (exported > 0) {
|
||||||
|
showToast(translate('Successful'));
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('Failed to export paths: $e');
|
||||||
|
showToast(translate('Failed'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@@ -159,6 +328,45 @@ class _FileManagerPageState extends State<FileManagerPage> {
|
|||||||
),
|
),
|
||||||
value: "refresh",
|
value: "refresh",
|
||||||
),
|
),
|
||||||
|
if (isAndroid)
|
||||||
|
PopupMenuItem(
|
||||||
|
enabled: showLocal && currentDir.path.isNotEmpty,
|
||||||
|
value: "import",
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.add_to_drive,
|
||||||
|
color: Theme.of(context).iconTheme.color),
|
||||||
|
SizedBox(width: 5),
|
||||||
|
Text(translate("Add"))
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (isAndroid)
|
||||||
|
PopupMenuItem(
|
||||||
|
enabled: showLocal && currentDir.path.isNotEmpty,
|
||||||
|
value: "import_folder",
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.create_new_folder_outlined,
|
||||||
|
color: Theme.of(context).iconTheme.color),
|
||||||
|
SizedBox(width: 5),
|
||||||
|
Text(translate("Import Folder"))
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (isAndroid)
|
||||||
|
PopupMenuItem(
|
||||||
|
enabled: showLocal && currentDir.path.isNotEmpty,
|
||||||
|
value: "export_logs",
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.article_outlined,
|
||||||
|
color: Theme.of(context).iconTheme.color),
|
||||||
|
SizedBox(width: 5),
|
||||||
|
Text(translate("Export Logs"))
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
enabled: currentDir.path != "/",
|
enabled: currentDir.path != "/",
|
||||||
child: Row(
|
child: Row(
|
||||||
@@ -203,6 +411,12 @@ class _FileManagerPageState extends State<FileManagerPage> {
|
|||||||
onSelected: (v) {
|
onSelected: (v) {
|
||||||
if (v == "refresh") {
|
if (v == "refresh") {
|
||||||
currentFileController.refresh();
|
currentFileController.refresh();
|
||||||
|
} else if (v == "import") {
|
||||||
|
_importFiles();
|
||||||
|
} else if (v == "import_folder") {
|
||||||
|
_importFolder();
|
||||||
|
} else if (v == "export_logs") {
|
||||||
|
_exportLogs();
|
||||||
} else if (v == "select") {
|
} else if (v == "select") {
|
||||||
model.localController.selectedItems.clear();
|
model.localController.selectedItems.clear();
|
||||||
model.remoteController.selectedItems.clear();
|
model.remoteController.selectedItems.clear();
|
||||||
@@ -300,6 +514,24 @@ class _FileManagerPageState extends State<FileManagerPage> {
|
|||||||
setState(() {});
|
setState(() {});
|
||||||
},
|
},
|
||||||
actions: [
|
actions: [
|
||||||
|
if (isAndroid &&
|
||||||
|
selectedItems?.isLocal == true &&
|
||||||
|
selectedItems?.items.isNotEmpty == true) ...[
|
||||||
|
if (selectedItems!.items.length == 1 &&
|
||||||
|
selectedItems!.items.single.isFile)
|
||||||
|
IconButton(
|
||||||
|
tooltip: translate("Save as"),
|
||||||
|
icon: Icon(Icons.save_alt),
|
||||||
|
onPressed: () =>
|
||||||
|
_exportFile(selectedItems!.items.single),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
IconButton(
|
||||||
|
tooltip: translate("Export"),
|
||||||
|
icon: Icon(Icons.drive_folder_upload),
|
||||||
|
onPressed: () => _exportItems(selectedItems!),
|
||||||
|
),
|
||||||
|
],
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: Icon(Icons.compare_arrows),
|
icon: Icon(Icons.compare_arrows),
|
||||||
onPressed: () => setState(() => showLocal = !showLocal),
|
onPressed: () => setState(() => showLocal = !showLocal),
|
||||||
@@ -366,8 +598,7 @@ class _FileManagerPageState extends State<FileManagerPage> {
|
|||||||
return BottomSheetBody(
|
return BottomSheetBody(
|
||||||
leading: CircularProgressIndicator(),
|
leading: CircularProgressIndicator(),
|
||||||
title: translate("Waiting"),
|
title: translate("Waiting"),
|
||||||
text:
|
text: "${readableFileSize(activeJob.speed)}/s",
|
||||||
"${translate("Speed")}: ${readableFileSize(activeJob.speed)}/s",
|
|
||||||
onCanceled: () {
|
onCanceled: () {
|
||||||
model.jobController.cancelJob(activeJob.id);
|
model.jobController.cancelJob(activeJob.id);
|
||||||
jobTable.clear();
|
jobTable.clear();
|
||||||
|
|||||||
@@ -1276,6 +1276,14 @@ void showOptions(
|
|||||||
List<TToggleMenu> cursorToggles = await toolbarCursor(context, id, gFFI);
|
List<TToggleMenu> cursorToggles = await toolbarCursor(context, id, gFFI);
|
||||||
List<TToggleMenu> displayToggles =
|
List<TToggleMenu> displayToggles =
|
||||||
await toolbarDisplayToggle(context, id, gFFI);
|
await toolbarDisplayToggle(context, id, gFFI);
|
||||||
|
if (isMobile) {
|
||||||
|
displayToggles.insert(
|
||||||
|
0,
|
||||||
|
TToggleMenu(
|
||||||
|
child: Text(translate('Lock canvas')),
|
||||||
|
value: gFFI.canvasModel.locked,
|
||||||
|
onChanged: (value) => gFFI.canvasModel.setLocked(value == true)));
|
||||||
|
}
|
||||||
|
|
||||||
List<TToggleMenu> privacyModeList = [];
|
List<TToggleMenu> privacyModeList = [];
|
||||||
if ((gFFI.ffiModel.pi.features.privacyMode && gFFI.ffiModel.keyboard) ||
|
if ((gFFI.ffiModel.pi.features.privacyMode && gFFI.ffiModel.keyboard) ||
|
||||||
|
|||||||
@@ -225,12 +225,6 @@ class _ServerPageState extends State<ServerPage> {
|
|||||||
|
|
||||||
void checkService() async {
|
void checkService() async {
|
||||||
gFFI.invokeMethod("check_service");
|
gFFI.invokeMethod("check_service");
|
||||||
// for Android 10/11, request MANAGE_EXTERNAL_STORAGE permission from system setting page
|
|
||||||
if (AndroidPermissionManager.isWaitingFile() && !gFFI.serverModel.fileOk) {
|
|
||||||
AndroidPermissionManager.complete(kManageExternalStorage,
|
|
||||||
await AndroidPermissionManager.check(kManageExternalStorage));
|
|
||||||
debugPrint("file permission finished");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class ServiceNotRunningNotification extends StatelessWidget {
|
class ServiceNotRunningNotification extends StatelessWidget {
|
||||||
|
|||||||
@@ -97,10 +97,12 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
|||||||
var _hideNetwork = false;
|
var _hideNetwork = false;
|
||||||
var _hideWebSocket = false;
|
var _hideWebSocket = false;
|
||||||
var _enableTrustedDevices = false;
|
var _enableTrustedDevices = false;
|
||||||
|
var _enableTcpPunch = false;
|
||||||
var _enableUdpPunch = false;
|
var _enableUdpPunch = false;
|
||||||
var _allowInsecureTlsFallback = false;
|
var _allowInsecureTlsFallback = false;
|
||||||
var _disableUdp = false;
|
var _disableUdp = false;
|
||||||
var _enableIpv6Punch = false;
|
var _enableIpv6Punch = false;
|
||||||
|
var _enableWebrtc = false;
|
||||||
var _isUsingPublicServer = false;
|
var _isUsingPublicServer = false;
|
||||||
var _allowAskForNoteAtEndOfConnection = false;
|
var _allowAskForNoteAtEndOfConnection = false;
|
||||||
var _preventSleepWhileConnected = true;
|
var _preventSleepWhileConnected = true;
|
||||||
@@ -141,8 +143,10 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
|||||||
bind.mainGetBuildinOption(key: kOptionHideWebSocketSetting) == 'Y' ||
|
bind.mainGetBuildinOption(key: kOptionHideWebSocketSetting) == 'Y' ||
|
||||||
isWeb;
|
isWeb;
|
||||||
_enableTrustedDevices = mainGetBoolOptionSync(kOptionEnableTrustedDevices);
|
_enableTrustedDevices = mainGetBoolOptionSync(kOptionEnableTrustedDevices);
|
||||||
|
_enableTcpPunch = mainGetLocalBoolOptionSync(kOptionEnableTcpPunch);
|
||||||
_enableUdpPunch = mainGetLocalBoolOptionSync(kOptionEnableUdpPunch);
|
_enableUdpPunch = mainGetLocalBoolOptionSync(kOptionEnableUdpPunch);
|
||||||
_enableIpv6Punch = mainGetLocalBoolOptionSync(kOptionEnableIpv6Punch);
|
_enableIpv6Punch = mainGetLocalBoolOptionSync(kOptionEnableIpv6Punch);
|
||||||
|
_enableWebrtc = mainGetLocalBoolOptionSync(kOptionEnableWebrtc);
|
||||||
_allowAskForNoteAtEndOfConnection =
|
_allowAskForNoteAtEndOfConnection =
|
||||||
mainGetLocalBoolOptionSync(kOptionAllowAskForNoteAtEndOfConnection);
|
mainGetLocalBoolOptionSync(kOptionAllowAskForNoteAtEndOfConnection);
|
||||||
_preventSleepWhileConnected =
|
_preventSleepWhileConnected =
|
||||||
@@ -815,31 +819,65 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
if (!incomingOnly)
|
||||||
|
SettingsTile.switchTile(
|
||||||
|
title: Text(translate('Enable TCP hole punching')),
|
||||||
|
initialValue: _enableTcpPunch,
|
||||||
|
onToggle: isOptionFixed(kOptionEnableTcpPunch)
|
||||||
|
? null
|
||||||
|
: (v) async {
|
||||||
|
await mainSetLocalBoolOption(kOptionEnableTcpPunch, v);
|
||||||
|
final newValue =
|
||||||
|
mainGetLocalBoolOptionSync(kOptionEnableTcpPunch);
|
||||||
|
setState(() {
|
||||||
|
_enableTcpPunch = newValue;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
if (!incomingOnly)
|
if (!incomingOnly)
|
||||||
SettingsTile.switchTile(
|
SettingsTile.switchTile(
|
||||||
title: Text(translate('Enable UDP hole punching')),
|
title: Text(translate('Enable UDP hole punching')),
|
||||||
initialValue: _enableUdpPunch,
|
initialValue: _enableUdpPunch,
|
||||||
onToggle: (v) async {
|
onToggle: isOptionFixed(kOptionEnableUdpPunch)
|
||||||
await mainSetLocalBoolOption(kOptionEnableUdpPunch, v);
|
? null
|
||||||
final newValue =
|
: (v) async {
|
||||||
mainGetLocalBoolOptionSync(kOptionEnableUdpPunch);
|
await mainSetLocalBoolOption(kOptionEnableUdpPunch, v);
|
||||||
setState(() {
|
final newValue =
|
||||||
_enableUdpPunch = newValue;
|
mainGetLocalBoolOptionSync(kOptionEnableUdpPunch);
|
||||||
});
|
setState(() {
|
||||||
},
|
_enableUdpPunch = newValue;
|
||||||
|
});
|
||||||
|
},
|
||||||
),
|
),
|
||||||
if (!incomingOnly)
|
if (!incomingOnly)
|
||||||
SettingsTile.switchTile(
|
SettingsTile.switchTile(
|
||||||
title: Text(translate('Enable IPv6 P2P connection')),
|
title: Text(translate('Enable IPv6 P2P connection')),
|
||||||
initialValue: _enableIpv6Punch,
|
initialValue: _enableIpv6Punch,
|
||||||
onToggle: (v) async {
|
onToggle: isOptionFixed(kOptionEnableIpv6Punch)
|
||||||
await mainSetLocalBoolOption(kOptionEnableIpv6Punch, v);
|
? null
|
||||||
final newValue =
|
: (v) async {
|
||||||
mainGetLocalBoolOptionSync(kOptionEnableIpv6Punch);
|
await mainSetLocalBoolOption(kOptionEnableIpv6Punch, v);
|
||||||
setState(() {
|
final newValue =
|
||||||
_enableIpv6Punch = newValue;
|
mainGetLocalBoolOptionSync(kOptionEnableIpv6Punch);
|
||||||
});
|
setState(() {
|
||||||
},
|
_enableIpv6Punch = newValue;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
if (!incomingOnly)
|
||||||
|
SettingsTile.switchTile(
|
||||||
|
title: Text(translate('Enable WebRTC P2P connection')),
|
||||||
|
initialValue: _enableWebrtc,
|
||||||
|
onToggle: isOptionFixed(kOptionEnableWebrtc)
|
||||||
|
? null
|
||||||
|
: (v) async {
|
||||||
|
await mainSetLocalBoolOption(kOptionEnableWebrtc, v);
|
||||||
|
final newValue =
|
||||||
|
mainGetLocalBoolOptionSync(kOptionEnableWebrtc);
|
||||||
|
setState(() {
|
||||||
|
_enableWebrtc = newValue;
|
||||||
|
});
|
||||||
|
},
|
||||||
),
|
),
|
||||||
SettingsTile(
|
SettingsTile(
|
||||||
title: Text(translate('Language')),
|
title: Text(translate('Language')),
|
||||||
@@ -1269,16 +1307,18 @@ 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 = getOtherDefaultSettingOption(key) == 'Y';
|
||||||
final isOptFixed = isOptionFixed(key);
|
final isOptFixed = isOtherDefaultSettingReadOnly(key);
|
||||||
return SettingsTile.switchTile(
|
return SettingsTile.switchTile(
|
||||||
initialValue: value,
|
initialValue: value,
|
||||||
title: Text(translate(label)),
|
title: Text(translate(label)),
|
||||||
onToggle: isOptFixed
|
onToggle: isOptFixed
|
||||||
? null
|
? null
|
||||||
: (b) async {
|
: (b) async {
|
||||||
await bind.mainSetUserDefaultOption(
|
await setOtherDefaultSettingOption(
|
||||||
key: key, value: b ? 'Y' : defaultOptionNo);
|
key,
|
||||||
|
b ? 'Y' : defaultOptionNo,
|
||||||
|
);
|
||||||
setState(() {});
|
setState(() {});
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/gestures.dart';
|
import 'package:flutter/gestures.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
@@ -8,7 +9,9 @@ import 'package:flutter_hbb/common/widgets/dialog.dart';
|
|||||||
import 'package:flutter_hbb/models/input_modifier_utils.dart';
|
import 'package:flutter_hbb/models/input_modifier_utils.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/terminal_copy_shortcut.dart';
|
||||||
import 'package:flutter_hbb/models/terminal_model.dart';
|
import 'package:flutter_hbb/models/terminal_model.dart';
|
||||||
|
import 'package:flutter_hbb/models/terminal_mouse_handler.dart';
|
||||||
import 'package:flutter_hbb/mobile/terminal_keyboard_utils.dart';
|
import 'package:flutter_hbb/mobile/terminal_keyboard_utils.dart';
|
||||||
import 'package:flutter_hbb/web/dummy.dart'
|
import 'package:flutter_hbb/web/dummy.dart'
|
||||||
if (dart.library.html) 'package:flutter_hbb/web/terminal_font.dart';
|
if (dart.library.html) 'package:flutter_hbb/web/terminal_font.dart';
|
||||||
@@ -17,6 +20,49 @@ import 'package:xterm/xterm.dart';
|
|||||||
import '../../desktop/pages/terminal_connection_manager.dart';
|
import '../../desktop/pages/terminal_connection_manager.dart';
|
||||||
import '../../consts.dart';
|
import '../../consts.dart';
|
||||||
|
|
||||||
|
const _terminalBackgroundOpacity = 0.7;
|
||||||
|
|
||||||
|
Widget _buildTerminalViewForPlatform({
|
||||||
|
required bool reportMouseInput,
|
||||||
|
required bool reportTouchInput,
|
||||||
|
required Terminal terminal,
|
||||||
|
required TerminalController controller,
|
||||||
|
required TerminalStyle textStyle,
|
||||||
|
required EdgeInsets padding,
|
||||||
|
required bool deleteDetection,
|
||||||
|
required Map<ShortcutActivator, Intent>? shortcuts,
|
||||||
|
required FocusOnKeyEventCallback onKeyEvent,
|
||||||
|
required void Function(TapDownDetails, CellOffset) onSecondaryTapDown,
|
||||||
|
}) {
|
||||||
|
if (reportMouseInput || reportTouchInput) {
|
||||||
|
return TerminalMouseInteraction(
|
||||||
|
terminal,
|
||||||
|
controller: controller,
|
||||||
|
autofocus: true,
|
||||||
|
textStyle: textStyle,
|
||||||
|
deleteDetection: deleteDetection,
|
||||||
|
reportTouchInput: reportTouchInput,
|
||||||
|
shortcuts: shortcuts,
|
||||||
|
onKeyEvent: onKeyEvent,
|
||||||
|
backgroundOpacity: _terminalBackgroundOpacity,
|
||||||
|
padding: padding,
|
||||||
|
onSecondaryTapDown: onSecondaryTapDown,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return TerminalView(
|
||||||
|
terminal,
|
||||||
|
controller: controller,
|
||||||
|
autofocus: true,
|
||||||
|
textStyle: textStyle,
|
||||||
|
deleteDetection: deleteDetection,
|
||||||
|
shortcuts: shortcuts,
|
||||||
|
onKeyEvent: onKeyEvent,
|
||||||
|
backgroundOpacity: _terminalBackgroundOpacity,
|
||||||
|
padding: padding,
|
||||||
|
onSecondaryTapDown: onSecondaryTapDown,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
class TerminalPage extends StatefulWidget {
|
class TerminalPage extends StatefulWidget {
|
||||||
const TerminalPage({
|
const TerminalPage({
|
||||||
Key? key,
|
Key? key,
|
||||||
@@ -39,6 +85,19 @@ class TerminalPage extends StatefulWidget {
|
|||||||
|
|
||||||
class _TerminalPageState extends State<TerminalPage>
|
class _TerminalPageState extends State<TerminalPage>
|
||||||
with AutomaticKeepAliveClientMixin, WidgetsBindingObserver {
|
with AutomaticKeepAliveClientMixin, WidgetsBindingObserver {
|
||||||
|
bool get _canConfigureTerminalClipboardPermission =>
|
||||||
|
canConfigureTerminalClipboardPermission(
|
||||||
|
settingsDisabled: bind.isDisableSettings(),
|
||||||
|
optionFixed: isOptionFixed(kOptionAllowTerminalClipboardWrite),
|
||||||
|
);
|
||||||
|
bool get _canHandleTerminalClipboardWriteRequest =>
|
||||||
|
canHandleTerminalClipboardWriteRequest(
|
||||||
|
localOption: bind.mainGetLocalOption(
|
||||||
|
key: kOptionAllowTerminalClipboardWrite,
|
||||||
|
),
|
||||||
|
canConfigurePermission: _canConfigureTerminalClipboardPermission,
|
||||||
|
);
|
||||||
|
|
||||||
late FFI _ffi;
|
late FFI _ffi;
|
||||||
late TerminalModel _terminalModel;
|
late TerminalModel _terminalModel;
|
||||||
double? _cellHeight;
|
double? _cellHeight;
|
||||||
@@ -55,6 +114,9 @@ class _TerminalPageState extends State<TerminalPage>
|
|||||||
// For iOS edge swipe gesture
|
// For iOS edge swipe gesture
|
||||||
double _swipeStartX = 0;
|
double _swipeStartX = 0;
|
||||||
double _swipeCurrentX = 0;
|
double _swipeCurrentX = 0;
|
||||||
|
ScaffoldFeatureController<MaterialBanner, MaterialBannerClosedReason>?
|
||||||
|
_terminalClipboardNoticeController;
|
||||||
|
final _terminalClipboardNotice = TerminalClipboardNoticeCoordinator<int>();
|
||||||
|
|
||||||
// For web only.
|
// For web only.
|
||||||
// 'monospace' does not work on web, use Google Fonts, `??` is only for null safety.
|
// 'monospace' does not work on web, use Google Fonts, `??` is only for null safety.
|
||||||
@@ -87,6 +149,12 @@ class _TerminalPageState extends State<TerminalPage>
|
|||||||
|
|
||||||
// Create terminal model with specific terminal ID
|
// Create terminal model with specific terminal ID
|
||||||
_terminalModel = TerminalModel(_ffi, widget.terminalId);
|
_terminalModel = TerminalModel(_ffi, widget.terminalId);
|
||||||
|
if (_canHandleTerminalClipboardWriteRequest) {
|
||||||
|
_terminalModel.onClipboardWriteBlocked =
|
||||||
|
_handleTerminalClipboardWriteBlocked;
|
||||||
|
_terminalModel.onClipboardWriteSucceeded =
|
||||||
|
_handleTerminalClipboardWriteSucceeded;
|
||||||
|
}
|
||||||
debugPrint(
|
debugPrint(
|
||||||
'[TerminalPage] Terminal model created for terminal ${widget.terminalId}');
|
'[TerminalPage] Terminal model created for terminal ${widget.terminalId}');
|
||||||
|
|
||||||
@@ -132,12 +200,144 @@ class _TerminalPageState extends State<TerminalPage>
|
|||||||
_ffi.ffiModel.updateEventListener(_ffi.sessionId, widget.id);
|
_ffi.ffiModel.updateEventListener(_ffi.sessionId, widget.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _handleTerminalClipboardWriteBlocked(String clipboardText) {
|
||||||
|
if (!mounted) return;
|
||||||
|
final option = bind.mainGetLocalOption(
|
||||||
|
key: kOptionAllowTerminalClipboardWrite,
|
||||||
|
);
|
||||||
|
final request = _terminalClipboardNotice.recordBlocked(
|
||||||
|
source: widget.terminalId,
|
||||||
|
text: clipboardText,
|
||||||
|
option: option,
|
||||||
|
canWrite: (_) => _canWriteTerminalClipboard,
|
||||||
|
);
|
||||||
|
if (request != null) _showTerminalClipboardNotice(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showTerminalClipboardNotice(
|
||||||
|
TerminalClipboardNoticeRequest<int> request,
|
||||||
|
) {
|
||||||
|
final controller = ScaffoldMessenger.of(context).showMaterialBanner(
|
||||||
|
MaterialBanner(
|
||||||
|
leading: const Icon(Icons.content_copy_outlined),
|
||||||
|
content: Text(translate(kTerminalClipboardNoticeMessageKey)),
|
||||||
|
actions: [
|
||||||
|
AnimatedBuilder(
|
||||||
|
animation: _terminalClipboardNotice,
|
||||||
|
builder: (_, __) => TextButton(
|
||||||
|
onPressed: _terminalClipboardNotice.canClaimAction
|
||||||
|
? _handleTerminalClipboardNegativeAction
|
||||||
|
: null,
|
||||||
|
child: Text(translate(request.negativeActionKey)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
AnimatedBuilder(
|
||||||
|
animation: _terminalClipboardNotice,
|
||||||
|
builder: (_, __) => TextButton(
|
||||||
|
onPressed: _terminalClipboardNotice.canClaimAction
|
||||||
|
? _handleTerminalClipboardPositiveAction
|
||||||
|
: null,
|
||||||
|
child: Text(translate(request.actionKey)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
_terminalClipboardNoticeController = controller;
|
||||||
|
unawaited(controller.closed.then<void>((_) {
|
||||||
|
if (identical(_terminalClipboardNoticeController, controller)) {
|
||||||
|
_terminalClipboardNoticeController = null;
|
||||||
|
_terminalClipboardNotice.noticeClosed();
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleTerminalClipboardNegativeAction() {
|
||||||
|
final request = _terminalClipboardNotice.claimCurrentAction();
|
||||||
|
if (request == null) return;
|
||||||
|
if (request.persistAllowed) {
|
||||||
|
unawaited(_declineTerminalClipboardWrite());
|
||||||
|
} else {
|
||||||
|
_closeTerminalClipboardNotice();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleTerminalClipboardPositiveAction() {
|
||||||
|
final request = _terminalClipboardNotice.claimCurrentAction();
|
||||||
|
if (request == null) return;
|
||||||
|
unawaited(_completeTerminalClipboardWrite(request));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get _canWriteTerminalClipboard =>
|
||||||
|
_canHandleTerminalClipboardWriteRequest &&
|
||||||
|
!_ffi.closed &&
|
||||||
|
_ffi.ffiModel.permissions['clipboard'] != false;
|
||||||
|
|
||||||
|
void _handleTerminalClipboardWriteSucceeded(String _) {
|
||||||
|
_closeTerminalClipboardNotice();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _declineTerminalClipboardWrite() async {
|
||||||
|
try {
|
||||||
|
await bind.mainSetLocalOption(
|
||||||
|
key: kOptionAllowTerminalClipboardWrite,
|
||||||
|
value: kTerminalClipboardWriteDenied,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
debugPrint(
|
||||||
|
'[TerminalPage] Failed to save terminal clipboard permission: $error');
|
||||||
|
return;
|
||||||
|
} finally {
|
||||||
|
_terminalClipboardNotice.releaseAction();
|
||||||
|
}
|
||||||
|
_closeTerminalClipboardNotice();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _completeTerminalClipboardWrite(
|
||||||
|
TerminalClipboardNoticeRequest<int> request,
|
||||||
|
) async {
|
||||||
|
var completed = false;
|
||||||
|
try {
|
||||||
|
completed = await completeTerminalClipboardWrite(
|
||||||
|
clipboardText: request.text,
|
||||||
|
canWrite: () => _canWriteTerminalClipboard,
|
||||||
|
writeClipboard: writeTerminalClipboard,
|
||||||
|
persistAllowed: request.persistAllowed
|
||||||
|
? () => bind.mainSetLocalOption(
|
||||||
|
key: kOptionAllowTerminalClipboardWrite,
|
||||||
|
value: kTerminalClipboardWriteAllowed,
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
debugPrint(
|
||||||
|
'[TerminalPage] Failed to complete terminal clipboard write: $error');
|
||||||
|
} finally {
|
||||||
|
_terminalClipboardNotice.releaseAction();
|
||||||
|
}
|
||||||
|
if (!completed) return;
|
||||||
|
_closeTerminalClipboardNotice();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _closeTerminalClipboardNotice() {
|
||||||
|
if (!_terminalClipboardNotice.beginClose()) return;
|
||||||
|
final controller = _terminalClipboardNoticeController;
|
||||||
|
if (controller == null) {
|
||||||
|
debugPrint('[TerminalPage] Clipboard notice controller is missing');
|
||||||
|
_terminalClipboardNotice.noticeClosed();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
controller.close();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
// Unregister terminal model from FFI
|
// Unregister terminal model from FFI
|
||||||
_ffi.unregisterTerminalModel(widget.terminalId);
|
_ffi.unregisterTerminalModel(widget.terminalId);
|
||||||
_terminalModel.dispose();
|
_terminalModel.dispose();
|
||||||
_keyboardDebounce?.cancel();
|
_keyboardDebounce?.cancel();
|
||||||
|
_terminalClipboardNotice.clear();
|
||||||
|
_terminalClipboardNoticeController?.close();
|
||||||
WidgetsBinding.instance.removeObserver(this);
|
WidgetsBinding.instance.removeObserver(this);
|
||||||
super.dispose();
|
super.dispose();
|
||||||
TerminalConnectionManager.releaseConnection(widget.id);
|
TerminalConnectionManager.releaseConnection(widget.id);
|
||||||
@@ -190,6 +390,7 @@ class _TerminalPageState extends State<TerminalPage>
|
|||||||
KeyEventResult _handleTerminalKeyEvent(FocusNode _, KeyEvent event) {
|
KeyEventResult _handleTerminalKeyEvent(FocusNode _, KeyEvent event) {
|
||||||
final hardwareKeyboard = HardwareKeyboard.instance;
|
final hardwareKeyboard = HardwareKeyboard.instance;
|
||||||
final shouldPaste = shouldHandleTerminalPasteShortcut(
|
final shouldPaste = shouldHandleTerminalPasteShortcut(
|
||||||
|
platform: defaultTargetPlatform,
|
||||||
logicalKey: event.logicalKey,
|
logicalKey: event.logicalKey,
|
||||||
isKeyDown: event is KeyDownEvent,
|
isKeyDown: event is KeyDownEvent,
|
||||||
isKeyRepeat: event is KeyRepeatEvent,
|
isKeyRepeat: event is KeyRepeatEvent,
|
||||||
@@ -231,12 +432,12 @@ class _TerminalPageState extends State<TerminalPage>
|
|||||||
child: LayoutBuilder(
|
child: LayoutBuilder(
|
||||||
builder: (context, constraints) {
|
builder: (context, constraints) {
|
||||||
final heightPx = constraints.maxHeight;
|
final heightPx = constraints.maxHeight;
|
||||||
return TerminalView(
|
return _buildTerminalViewForPlatform(
|
||||||
_terminalModel.terminal,
|
reportMouseInput: isWebDesktop || isAndroid,
|
||||||
|
reportTouchInput: isIOS,
|
||||||
|
terminal: _terminalModel.terminal,
|
||||||
controller: _terminalModel.terminalController,
|
controller: _terminalModel.terminalController,
|
||||||
autofocus: true,
|
|
||||||
textStyle: _getTerminalStyle(),
|
textStyle: _getTerminalStyle(),
|
||||||
backgroundOpacity: 0.7,
|
|
||||||
// The following comment is from xterm.dart source code:
|
// The following comment is from xterm.dart source code:
|
||||||
// Workaround to detect delete key for platforms and IMEs that do not
|
// Workaround to detect delete key for platforms and IMEs that do not
|
||||||
// emit a hardware delete event. Preferred on mobile platforms. [false] by
|
// emit a hardware delete event. Preferred on mobile platforms. [false] by
|
||||||
@@ -244,7 +445,12 @@ class _TerminalPageState extends State<TerminalPage>
|
|||||||
//
|
//
|
||||||
// Android works fine without this workaround.
|
// Android works fine without this workaround.
|
||||||
deleteDetection: isIOS,
|
deleteDetection: isIOS,
|
||||||
onKeyEvent: _handleTerminalKeyEvent,
|
shortcuts: platformTerminalShortcuts(),
|
||||||
|
onKeyEvent: terminalCopyHandler(
|
||||||
|
_terminalModel.terminal,
|
||||||
|
_terminalModel.terminalController,
|
||||||
|
fallback: _handleTerminalKeyEvent,
|
||||||
|
),
|
||||||
padding: _calculatePadding(heightPx),
|
padding: _calculatePadding(heightPx),
|
||||||
onSecondaryTapDown: (details, offset) async {
|
onSecondaryTapDown: (details, offset) async {
|
||||||
final selection = _terminalModel.terminalController.selection;
|
final selection = _terminalModel.terminalController.selection;
|
||||||
|
|||||||
@@ -16,8 +16,6 @@ class _PixelbufferTexture {
|
|||||||
int _display = 0;
|
int _display = 0;
|
||||||
SessionID? _sessionId;
|
SessionID? _sessionId;
|
||||||
bool _destroying = false;
|
bool _destroying = false;
|
||||||
bool _closed = false;
|
|
||||||
int _ptr = 0;
|
|
||||||
int? _id;
|
int? _id;
|
||||||
|
|
||||||
final textureRenderer = TextureRgbaRenderer();
|
final textureRenderer = TextureRgbaRenderer();
|
||||||
@@ -29,22 +27,11 @@ class _PixelbufferTexture {
|
|||||||
_textureKey = bind.getNextTextureKey();
|
_textureKey = bind.getNextTextureKey();
|
||||||
_sessionId = sessionId;
|
_sessionId = sessionId;
|
||||||
|
|
||||||
final textureKey = _textureKey;
|
textureRenderer.createTexture(_textureKey).then((id) async {
|
||||||
textureRenderer.createTexture(textureKey).then((id) async {
|
|
||||||
_id = id;
|
_id = id;
|
||||||
if (id != -1) {
|
if (id != -1) {
|
||||||
if (_closed) {
|
|
||||||
// Destroyed while creation was still in flight (rapid
|
|
||||||
// connect/disconnect); nobody else will close this texture.
|
|
||||||
await textureRenderer.closeTexture(textureKey);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
ffi.textureModel.setRgbaTextureId(display: d, id: id);
|
ffi.textureModel.setRgbaTextureId(display: d, id: id);
|
||||||
final ptr = await textureRenderer.getTexturePtr(textureKey);
|
final ptr = await textureRenderer.getTexturePtr(_textureKey);
|
||||||
if (_closed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
_ptr = ptr;
|
|
||||||
platformFFI.registerPixelbufferTexture(sessionId, display, ptr);
|
platformFFI.registerPixelbufferTexture(sessionId, display, ptr);
|
||||||
debugPrint(
|
debugPrint(
|
||||||
"create pixelbuffer texture: peerId: ${ffi.id} display:$_display, textureId:$id, texturePtr:$ptr");
|
"create pixelbuffer texture: peerId: ${ffi.id} display:$_display, textureId:$id, texturePtr:$ptr");
|
||||||
@@ -52,16 +39,13 @@ class _PixelbufferTexture {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy(FFI ffi) async {
|
destroy(bool unregisterTexture, FFI ffi) async {
|
||||||
_closed = true;
|
|
||||||
if (!_destroying && _textureKey != -1 && _sessionId != null) {
|
if (!_destroying && _textureKey != -1 && _sessionId != null) {
|
||||||
_destroying = true;
|
_destroying = true;
|
||||||
if (_ptr != 0) {
|
if (unregisterTexture) {
|
||||||
// Compare-and-clear: only clears if Rust still holds this pointer
|
platformFFI.registerPixelbufferTexture(_sessionId!, display, 0);
|
||||||
// (#8016-safe); returning from this synchronous call also means no
|
// sleep for a while to avoid the texture is used after it's unregistered.
|
||||||
// push through the old pointer is still in flight.
|
await Future.delayed(Duration(milliseconds: 100));
|
||||||
platformFFI.unregisterPixelbufferTexture(_sessionId!, display, _ptr);
|
|
||||||
_ptr = 0;
|
|
||||||
}
|
}
|
||||||
await textureRenderer.closeTexture(_textureKey);
|
await textureRenderer.closeTexture(_textureKey);
|
||||||
_textureKey = -1;
|
_textureKey = -1;
|
||||||
@@ -77,7 +61,6 @@ class _GpuTexture {
|
|||||||
SessionID? _sessionId;
|
SessionID? _sessionId;
|
||||||
final support = bind.mainHasGpuTextureRender();
|
final support = bind.mainHasGpuTextureRender();
|
||||||
bool _destroying = false;
|
bool _destroying = false;
|
||||||
bool _closed = false;
|
|
||||||
int _display = 0;
|
int _display = 0;
|
||||||
int? _id;
|
int? _id;
|
||||||
int? _output;
|
int? _output;
|
||||||
@@ -96,18 +79,9 @@ class _GpuTexture {
|
|||||||
gpuTextureRenderer.registerTexture().then((id) async {
|
gpuTextureRenderer.registerTexture().then((id) async {
|
||||||
_id = id;
|
_id = id;
|
||||||
if (id != null) {
|
if (id != null) {
|
||||||
if (_closed) {
|
|
||||||
// Destroyed while creation was still in flight (rapid
|
|
||||||
// connect/disconnect); nobody else will unregister this texture.
|
|
||||||
await gpuTextureRenderer.unregisterTexture(id);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
_textureId = id;
|
_textureId = id;
|
||||||
ffi.textureModel.setGpuTextureId(display: d, id: id);
|
ffi.textureModel.setGpuTextureId(display: d, id: id);
|
||||||
final output = await gpuTextureRenderer.output(id);
|
final output = await gpuTextureRenderer.output(id);
|
||||||
if (_closed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
_output = output;
|
_output = output;
|
||||||
if (output != null) {
|
if (output != null) {
|
||||||
platformFFI.registerGpuTexture(sessionId, d, output);
|
platformFFI.registerGpuTexture(sessionId, d, output);
|
||||||
@@ -121,22 +95,20 @@ class _GpuTexture {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy(FFI ffi) async {
|
destroy(bool unregisterTexture, FFI ffi) async {
|
||||||
// must stop texture render, render unregistered texture cause crash
|
// must stop texture render, render unregistered texture cause crash
|
||||||
_closed = true;
|
|
||||||
if (!_destroying && support && _sessionId != null && _textureId != -1) {
|
if (!_destroying && support && _sessionId != null && _textureId != -1) {
|
||||||
_destroying = true;
|
_destroying = true;
|
||||||
final output = _output;
|
if (unregisterTexture) {
|
||||||
if (output != null) {
|
platformFFI.registerGpuTexture(_sessionId!, _display, 0);
|
||||||
// Compare-and-clear, see _PixelbufferTexture.destroy.
|
// sleep for a while to avoid the texture is used after it's unregistered.
|
||||||
platformFFI.unregisterGpuTexture(_sessionId!, _display, output);
|
await Future.delayed(Duration(milliseconds: 100));
|
||||||
_output = null;
|
|
||||||
}
|
}
|
||||||
await gpuTextureRenderer.unregisterTexture(_textureId);
|
await gpuTextureRenderer.unregisterTexture(_textureId);
|
||||||
_textureId = -1;
|
_textureId = -1;
|
||||||
_destroying = false;
|
_destroying = false;
|
||||||
debugPrint(
|
debugPrint(
|
||||||
"destroy gpu texture: peerId: ${ffi.id} display:$_display, textureId:$_id, output:$output");
|
"destroy gpu texture: peerId: ${ffi.id} display:$_display, textureId:$_id, output:$_output");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -228,11 +200,11 @@ class TextureModel {
|
|||||||
tryRemoveTexture(int idx) {
|
tryRemoveTexture(int idx) {
|
||||||
_control.remove(idx);
|
_control.remove(idx);
|
||||||
if (_pixelbufferRenderTextures.containsKey(idx)) {
|
if (_pixelbufferRenderTextures.containsKey(idx)) {
|
||||||
_pixelbufferRenderTextures[idx]!.destroy(ffi);
|
_pixelbufferRenderTextures[idx]!.destroy(true, ffi);
|
||||||
_pixelbufferRenderTextures.remove(idx);
|
_pixelbufferRenderTextures.remove(idx);
|
||||||
}
|
}
|
||||||
if (_gpuRenderTextures.containsKey(idx)) {
|
if (_gpuRenderTextures.containsKey(idx)) {
|
||||||
_gpuRenderTextures[idx]!.destroy(ffi);
|
_gpuRenderTextures[idx]!.destroy(true, ffi);
|
||||||
_gpuRenderTextures.remove(idx);
|
_gpuRenderTextures.remove(idx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -252,25 +224,25 @@ class TextureModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onRemotePageDispose() async {
|
onRemotePageDispose(bool closeSession) async {
|
||||||
final ffi = parent.target;
|
final ffi = parent.target;
|
||||||
if (ffi == null) return;
|
if (ffi == null) return;
|
||||||
for (final texture in _pixelbufferRenderTextures.values) {
|
for (final texture in _pixelbufferRenderTextures.values) {
|
||||||
await texture.destroy(ffi);
|
await texture.destroy(closeSession, ffi);
|
||||||
}
|
}
|
||||||
for (final texture in _gpuRenderTextures.values) {
|
for (final texture in _gpuRenderTextures.values) {
|
||||||
await texture.destroy(ffi);
|
await texture.destroy(closeSession, ffi);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onViewCameraPageDispose() async {
|
onViewCameraPageDispose(bool closeSession) async {
|
||||||
final ffi = parent.target;
|
final ffi = parent.target;
|
||||||
if (ffi == null) return;
|
if (ffi == null) return;
|
||||||
for (final texture in _pixelbufferRenderTextures.values) {
|
for (final texture in _pixelbufferRenderTextures.values) {
|
||||||
await texture.destroy(ffi);
|
await texture.destroy(closeSession, ffi);
|
||||||
}
|
}
|
||||||
for (final texture in _gpuRenderTextures.values) {
|
for (final texture in _gpuRenderTextures.values) {
|
||||||
await texture.destroy(ffi);
|
await texture.destroy(closeSession, ffi);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -46,6 +46,12 @@ class JobID {
|
|||||||
|
|
||||||
typedef GetSessionID = SessionID Function();
|
typedef GetSessionID = SessionID Function();
|
||||||
typedef GetDialogManager = OverlayDialogManager? Function();
|
typedef GetDialogManager = OverlayDialogManager? Function();
|
||||||
|
typedef ReadRemoteDirectory = Future<void> Function(
|
||||||
|
SessionID sessionId, String path, bool includeHidden);
|
||||||
|
|
||||||
|
const _kRemoteReadDirTimeout = Duration(seconds: 30);
|
||||||
|
const _kRemoteSessionChangedError =
|
||||||
|
'Remote directory read cancelled because the session changed';
|
||||||
|
|
||||||
class FileModel {
|
class FileModel {
|
||||||
final WeakReference<FFI> parent;
|
final WeakReference<FFI> parent;
|
||||||
@@ -84,6 +90,7 @@ class FileModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> onReady() async {
|
Future<void> onReady() async {
|
||||||
|
fileFetcher.beginRemoteSession();
|
||||||
await evtLoop.onReady();
|
await evtLoop.onReady();
|
||||||
if (!isWeb) await localController.onReady();
|
if (!isWeb) await localController.onReady();
|
||||||
await remoteController.onReady();
|
await remoteController.onReady();
|
||||||
@@ -133,7 +140,11 @@ class FileModel {
|
|||||||
final id = int.tryParse(evt['id']?.toString() ?? '');
|
final id = int.tryParse(evt['id']?.toString() ?? '');
|
||||||
if (id != null) {
|
if (id != null) {
|
||||||
final err = evt['err']?.toString() ?? 'Unknown error';
|
final err = evt['err']?.toString() ?? 'Unknown error';
|
||||||
fileFetcher.tryCompleteRecursiveTaskWithError(id, err);
|
if (id == 0) {
|
||||||
|
fileFetcher.tryCompleteRemoteTaskWithError(err);
|
||||||
|
} else {
|
||||||
|
fileFetcher.tryCompleteRecursiveTaskWithError(id, err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Always call jobController.jobError(evt) to ensure all error events are processed,
|
// Always call jobController.jobError(evt) to ensure all error events are processed,
|
||||||
// even if the event does not have a valid job ID. This allows for generic error handling
|
// even if the event does not have a valid job ID. This allows for generic error handling
|
||||||
@@ -350,6 +361,8 @@ class FileController {
|
|||||||
final history = RxList<String>.empty(growable: true);
|
final history = RxList<String>.empty(growable: true);
|
||||||
final sortBy = SortBy.name.obs;
|
final sortBy = SortBy.name.obs;
|
||||||
var sortAscending = true;
|
var sortAscending = true;
|
||||||
|
// Incremented for each navigation; only the latest generation applies results.
|
||||||
|
int _directoryRequestGeneration = 0;
|
||||||
final JobController jobController;
|
final JobController jobController;
|
||||||
final WeakReference<FFI> rootState;
|
final WeakReference<FFI> rootState;
|
||||||
|
|
||||||
@@ -368,6 +381,14 @@ class FileController {
|
|||||||
void set homePath(String path) => options.value.home = path;
|
void set homePath(String path) => options.value.home = path;
|
||||||
OverlayDialogManager? get dialogManager => rootState.target?.dialogManager;
|
OverlayDialogManager? get dialogManager => rootState.target?.dialogManager;
|
||||||
|
|
||||||
|
bool _isPathAllowed(String candidate) {
|
||||||
|
if (!isAndroid || !isLocal) return true;
|
||||||
|
if (homePath.isEmpty || candidate.isEmpty) return false;
|
||||||
|
final home = PathUtil.posixContext.normalize(homePath);
|
||||||
|
final target = PathUtil.posixContext.normalize(candidate);
|
||||||
|
return target == home || PathUtil.posixContext.isWithin(home, target);
|
||||||
|
}
|
||||||
|
|
||||||
String get shortPath {
|
String get shortPath {
|
||||||
final dirPath = directory.value.path;
|
final dirPath = directory.value.path;
|
||||||
if (dirPath.startsWith(homePath)) {
|
if (dirPath.startsWith(homePath)) {
|
||||||
@@ -401,8 +422,13 @@ class FileController {
|
|||||||
|
|
||||||
await Future.delayed(Duration(milliseconds: 100));
|
await Future.delayed(Duration(milliseconds: 100));
|
||||||
|
|
||||||
final savedDir = (await bind.sessionGetPeerOption(
|
var savedDir = (await bind.sessionGetPeerOption(
|
||||||
sessionId: sessionId, name: isLocal ? "local_dir" : "remote_dir"));
|
sessionId: sessionId, name: isLocal ? "local_dir" : "remote_dir"));
|
||||||
|
if (savedDir.isNotEmpty && !_isPathAllowed(savedDir)) {
|
||||||
|
savedDir = options.value.home;
|
||||||
|
await bind.sessionPeerOption(
|
||||||
|
sessionId: sessionId, name: "local_dir", value: savedDir);
|
||||||
|
}
|
||||||
Future<bool> tryOpenReadyDirs() async {
|
Future<bool> tryOpenReadyDirs() async {
|
||||||
final dirs = <String>{
|
final dirs = <String>{
|
||||||
if (directory.value.path.isNotEmpty) directory.value.path,
|
if (directory.value.path.isNotEmpty) directory.value.path,
|
||||||
@@ -472,6 +498,9 @@ class FileController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> _openDirectoryPath(String path, {bool isBack = false}) async {
|
Future<bool> _openDirectoryPath(String path, {bool isBack = false}) async {
|
||||||
|
if (!_isPathAllowed(path)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if (!isBack) {
|
if (!isBack) {
|
||||||
pushHistory();
|
pushHistory();
|
||||||
}
|
}
|
||||||
@@ -484,12 +513,20 @@ class FileController {
|
|||||||
path = "$path\\";
|
path = "$path\\";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
final requestGeneration = ++_directoryRequestGeneration;
|
||||||
try {
|
try {
|
||||||
final fd = await fileFetcher.fetchDirectory(path, isLocal, showHidden);
|
final fd = await fileFetcher.fetchDirectory(path, isLocal, showHidden);
|
||||||
|
if (requestGeneration != _directoryRequestGeneration) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
fd.format(isWindows, sort: sortBy.value);
|
fd.format(isWindows, sort: sortBy.value);
|
||||||
|
selectedItems.reconcile(fd.entries);
|
||||||
directory.value = fd;
|
directory.value = fd;
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
if (requestGeneration != _directoryRequestGeneration) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
debugPrint("Failed to openDirectory $path: $e");
|
debugPrint("Failed to openDirectory $path: $e");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -530,6 +567,9 @@ class FileController {
|
|||||||
final isWindows = options.value.isWindows;
|
final isWindows = options.value.isWindows;
|
||||||
final dirPath = directory.value.path;
|
final dirPath = directory.value.path;
|
||||||
var parent = PathUtil.dirname(dirPath, isWindows);
|
var parent = PathUtil.dirname(dirPath, isWindows);
|
||||||
|
if (!_isPathAllowed(parent)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
// specially for C:\, D:\, goto '/'
|
// specially for C:\, D:\, goto '/'
|
||||||
if (parent == dirPath && isWindows) {
|
if (parent == dirPath && isWindows) {
|
||||||
return await _openDirectoryPath('/', isBack: isBack);
|
return await _openDirectoryPath('/', isBack: isBack);
|
||||||
@@ -541,6 +581,7 @@ class FileController {
|
|||||||
void initDirAndHome(Map<String, dynamic> evt) {
|
void initDirAndHome(Map<String, dynamic> evt) {
|
||||||
try {
|
try {
|
||||||
final fd = FileDirectory.fromJson(jsonDecode(evt['value']));
|
final fd = FileDirectory.fromJson(jsonDecode(evt['value']));
|
||||||
|
final isHomeResponse = fileFetcher.isLikelyRemoteHomeResponse(fd.path);
|
||||||
fd.format(options.value.isWindows, sort: sortBy.value);
|
fd.format(options.value.isWindows, sort: sortBy.value);
|
||||||
if (fd.id > 0) {
|
if (fd.id > 0) {
|
||||||
final jobIndex = jobController.getJob(fd.id);
|
final jobIndex = jobController.getJob(fd.id);
|
||||||
@@ -556,10 +597,12 @@ class FileController {
|
|||||||
debugPrint("update receive details: ${fd.path}");
|
debugPrint("update receive details: ${fd.path}");
|
||||||
jobController.jobTable.refresh();
|
jobController.jobTable.refresh();
|
||||||
}
|
}
|
||||||
} else if (options.value.home.isEmpty) {
|
} else if (options.value.home.isEmpty && isHomeResponse) {
|
||||||
options.value.home = fd.path;
|
options.value.home = fd.path;
|
||||||
debugPrint("init remote home: ${fd.path}");
|
debugPrint("init remote home: ${fd.path}");
|
||||||
directory.value = fd;
|
if (_directoryRequestGeneration == 0) {
|
||||||
|
directory.value = fd;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("initDirAndHome err=$e");
|
debugPrint("initDirAndHome err=$e");
|
||||||
@@ -1362,16 +1405,78 @@ class JobResultListener<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _RemoteReadTask {
|
||||||
|
final bool includeHidden;
|
||||||
|
final Completer<FileDirectory> completer = Completer<FileDirectory>();
|
||||||
|
final Completer<void> released = Completer<void>();
|
||||||
|
late final Timer timer;
|
||||||
|
|
||||||
|
_RemoteReadTask(this.includeHidden);
|
||||||
|
}
|
||||||
|
|
||||||
class FileFetcher {
|
class FileFetcher {
|
||||||
// Map<String,Completer<FileDirectory>> localTasks = {}; // now we only use read local dir sync
|
// Map<String,Completer<FileDirectory>> localTasks = {}; // now we only use read local dir sync
|
||||||
Map<String, Completer<FileDirectory>> remoteTasks = {};
|
final Map<String, _RemoteReadTask> _remoteReadTasks = {};
|
||||||
Map<String, Completer<List<FileDirectory>>> remoteEmptyDirsTasks = {};
|
Map<String, Completer<List<FileDirectory>>> remoteEmptyDirsTasks = {};
|
||||||
Map<int, Completer<FileDirectory>> readRecursiveTasks = {};
|
Map<int, Completer<FileDirectory>> readRecursiveTasks = {};
|
||||||
|
int _remoteSessionGeneration = 0;
|
||||||
|
|
||||||
final GetSessionID getSessionID;
|
final GetSessionID getSessionID;
|
||||||
|
final ReadRemoteDirectory _readRemoteDirectory;
|
||||||
SessionID get sessionId => getSessionID();
|
SessionID get sessionId => getSessionID();
|
||||||
|
|
||||||
FileFetcher(this.getSessionID);
|
FileFetcher(this.getSessionID, {ReadRemoteDirectory? readRemoteDirectory})
|
||||||
|
: _readRemoteDirectory = readRemoteDirectory ??
|
||||||
|
((sessionId, path, includeHidden) => bind.sessionReadRemoteDir(
|
||||||
|
sessionId: sessionId,
|
||||||
|
path: path,
|
||||||
|
includeHidden: includeHidden));
|
||||||
|
|
||||||
|
bool hasPendingRemoteRead(String path) => _remoteReadTasks.containsKey(path);
|
||||||
|
|
||||||
|
bool isLikelyRemoteHomeResponse(String path) =>
|
||||||
|
_remoteReadTasks.isEmpty ||
|
||||||
|
(_remoteReadTasks.length == 1 &&
|
||||||
|
hasPendingRemoteRead("") &&
|
||||||
|
!hasPendingRemoteRead(path));
|
||||||
|
|
||||||
|
void beginRemoteSession() {
|
||||||
|
_remoteSessionGeneration++;
|
||||||
|
final pendingTasks = _remoteReadTasks.entries.toList(growable: false);
|
||||||
|
for (final entry in pendingTasks) {
|
||||||
|
final task = entry.value;
|
||||||
|
if (!_removeRemoteReadTask(entry.key, task)) continue;
|
||||||
|
task.completer.completeError(StateError(_kRemoteSessionChangedError));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_RemoteReadTask _registerRemoteReadTask(String path, bool includeHidden) {
|
||||||
|
if (hasPendingRemoteRead(path)) {
|
||||||
|
throw "Failed to registerReadTask, already have same read job";
|
||||||
|
}
|
||||||
|
final task = _RemoteReadTask(includeHidden);
|
||||||
|
_remoteReadTasks[path] = task;
|
||||||
|
task.timer = Timer(_kRemoteReadDirTimeout, () {
|
||||||
|
if (!_removeRemoteReadTask(path, task)) return;
|
||||||
|
task.completer.completeError("Failed to read dir, timeout");
|
||||||
|
});
|
||||||
|
return task;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _removeRemoteReadTask(String path, _RemoteReadTask task) {
|
||||||
|
if (!identical(_remoteReadTasks[path], task)) return false;
|
||||||
|
_remoteReadTasks.remove(path);
|
||||||
|
task.timer.cancel();
|
||||||
|
task.released.complete();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _completeRemoteReadTask(String path, FileDirectory directory) {
|
||||||
|
final task = _remoteReadTasks[path];
|
||||||
|
if (task == null || !_removeRemoteReadTask(path, task)) return false;
|
||||||
|
task.completer.complete(directory);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
Future<List<FileDirectory>> registerReadEmptyDirsTask(
|
Future<List<FileDirectory>> registerReadEmptyDirsTask(
|
||||||
bool isLocal, String path) {
|
bool isLocal, String path) {
|
||||||
@@ -1391,23 +1496,6 @@ class FileFetcher {
|
|||||||
return c.future;
|
return c.future;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<FileDirectory> registerReadTask(bool isLocal, String path) {
|
|
||||||
// final jobs = isLocal?localJobs:remoteJobs; // maybe we will use read local dir async later
|
|
||||||
final tasks = remoteTasks; // bypass now
|
|
||||||
if (tasks.containsKey(path)) {
|
|
||||||
throw "Failed to registerReadTask, already have same read job";
|
|
||||||
}
|
|
||||||
final c = Completer<FileDirectory>();
|
|
||||||
tasks[path] = c;
|
|
||||||
|
|
||||||
Timer(Duration(seconds: 2), () {
|
|
||||||
tasks.remove(path);
|
|
||||||
if (c.isCompleted) return;
|
|
||||||
c.completeError("Failed to read dir, timeout");
|
|
||||||
});
|
|
||||||
return c.future;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<FileDirectory> registerReadRecursiveTask(int actID) {
|
Future<FileDirectory> registerReadRecursiveTask(int actID) {
|
||||||
final tasks = readRecursiveTasks;
|
final tasks = readRecursiveTasks;
|
||||||
if (tasks.containsKey(actID)) {
|
if (tasks.containsKey(actID)) {
|
||||||
@@ -1445,27 +1533,37 @@ class FileFetcher {
|
|||||||
|
|
||||||
tryCompleteTask(String? msg, String? isLocalStr) {
|
tryCompleteTask(String? msg, String? isLocalStr) {
|
||||||
if (msg == null || isLocalStr == null) return;
|
if (msg == null || isLocalStr == null) return;
|
||||||
late final Map<Object, Completer<FileDirectory>> tasks;
|
|
||||||
try {
|
try {
|
||||||
final fd = FileDirectory.fromJson(jsonDecode(msg));
|
final fd = FileDirectory.fromJson(jsonDecode(msg));
|
||||||
if (fd.id > 0) {
|
if (fd.id > 0) {
|
||||||
// fd.id > 0 is result for read recursive
|
// fd.id > 0 is result for read recursive
|
||||||
// to-do later,will be better if every fetch use ID,so that there will only one task map for read and recursive read
|
final completer = readRecursiveTasks.remove(fd.id);
|
||||||
tasks = readRecursiveTasks;
|
|
||||||
final completer = tasks.remove(fd.id);
|
|
||||||
completer?.complete(fd);
|
|
||||||
} else if (fd.path.isNotEmpty) {
|
|
||||||
// result for normal read dir
|
|
||||||
// final jobs = isLocal?localJobs:remoteJobs; // maybe we will use read local dir async later
|
|
||||||
tasks = remoteTasks; // bypass now
|
|
||||||
final completer = tasks.remove(fd.path);
|
|
||||||
completer?.complete(fd);
|
completer?.complete(fd);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isLocalStr == "false" && fd.path.isNotEmpty) {
|
||||||
|
if (_completeRemoteReadTask(fd.path, fd)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// A Home request uses an empty path but returns its resolved path.
|
||||||
|
if (isLikelyRemoteHomeResponse(fd.path)) {
|
||||||
|
_completeRemoteReadTask("", fd);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("tryCompleteJob err: $e");
|
debugPrint("tryCompleteJob err: $e");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool tryCompleteRemoteTaskWithError(String error) {
|
||||||
|
if (_remoteReadTasks.length != 1) return false;
|
||||||
|
final entry = _remoteReadTasks.entries.single;
|
||||||
|
final task = entry.value;
|
||||||
|
if (!_removeRemoteReadTask(entry.key, task)) return false;
|
||||||
|
task.completer.completeError(error);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// Complete a pending recursive read task with an error.
|
// Complete a pending recursive read task with an error.
|
||||||
// See FileModel.handleJobError() for why this is necessary.
|
// See FileModel.handleJobError() for why this is necessary.
|
||||||
void tryCompleteRecursiveTaskWithError(int id, String error) {
|
void tryCompleteRecursiveTaskWithError(int id, String error) {
|
||||||
@@ -1506,9 +1604,26 @@ class FileFetcher {
|
|||||||
final fd = FileDirectory.fromJson(jsonDecode(res));
|
final fd = FileDirectory.fromJson(jsonDecode(res));
|
||||||
return fd;
|
return fd;
|
||||||
} else {
|
} else {
|
||||||
await bind.sessionReadRemoteDir(
|
final remoteSessionGeneration = _remoteSessionGeneration;
|
||||||
sessionId: sessionId, path: path, includeHidden: showHidden);
|
final pendingTask = _remoteReadTasks[path];
|
||||||
return registerReadTask(isLocal, path);
|
if (pendingTask != null) {
|
||||||
|
if (pendingTask.includeHidden == showHidden) {
|
||||||
|
return pendingTask.completer.future;
|
||||||
|
}
|
||||||
|
await pendingTask.released.future;
|
||||||
|
if (remoteSessionGeneration != _remoteSessionGeneration) {
|
||||||
|
throw StateError(_kRemoteSessionChangedError);
|
||||||
|
}
|
||||||
|
return fetchDirectory(path, isLocal, showHidden);
|
||||||
|
}
|
||||||
|
final task = _registerRemoteReadTask(path, showHidden);
|
||||||
|
unawaited(Future<void>.sync(
|
||||||
|
() => _readRemoteDirectory(sessionId, path, showHidden))
|
||||||
|
.catchError((Object error, StackTrace stackTrace) {
|
||||||
|
if (!_removeRemoteReadTask(path, task)) return;
|
||||||
|
task.completer.completeError(error, stackTrace);
|
||||||
|
}));
|
||||||
|
return task.completer.future;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return Future.error(e);
|
return Future.error(e);
|
||||||
@@ -1790,7 +1905,7 @@ class PathUtil {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static bool validName(String name, bool isWindows) {
|
static bool validName(String name, bool isWindows) {
|
||||||
final unixFileNamePattern = RegExp(r'^[^/\0]+$');
|
final unixFileNamePattern = RegExp(r'^[^/\x00]+$');
|
||||||
final windowsFileNamePattern = RegExp(r'^[^<>:"/\\|?*]+$');
|
final windowsFileNamePattern = RegExp(r'^[^<>:"/\\|?*]+$');
|
||||||
final reg = isWindows ? windowsFileNamePattern : unixFileNamePattern;
|
final reg = isWindows ? windowsFileNamePattern : unixFileNamePattern;
|
||||||
return reg.hasMatch(name);
|
return reg.hasMatch(name);
|
||||||
@@ -1833,6 +1948,21 @@ class SelectedItems {
|
|||||||
items.clear();
|
items.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void reconcile(List<Entry> entries) {
|
||||||
|
if (items.isEmpty) return;
|
||||||
|
final currentByPath = {for (final entry in entries) entry.path: entry};
|
||||||
|
final reconciled = <Entry>[];
|
||||||
|
for (final item in items) {
|
||||||
|
final current = currentByPath[item.path];
|
||||||
|
if (current != null && current.entryType == item.entryType) {
|
||||||
|
reconciled.add(current);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
items
|
||||||
|
..clear()
|
||||||
|
..addAll(reconciled);
|
||||||
|
}
|
||||||
|
|
||||||
void selectAll(List<Entry> entries) {
|
void selectAll(List<Entry> entries) {
|
||||||
items.clear();
|
items.clear();
|
||||||
items.addAll(entries);
|
items.addAll(entries);
|
||||||
|
|||||||
@@ -1787,6 +1787,11 @@ class InputModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool _checkPeerControlProtected(double x, double y) {
|
bool _checkPeerControlProtected(double x, double y) {
|
||||||
|
if (isViewOnly && showMyCursor) {
|
||||||
|
lastMousePos = ui.Offset(x, y);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
final cursorModel = parent.target!.cursorModel;
|
final cursorModel = parent.target!.cursorModel;
|
||||||
if (cursorModel.isPeerControlProtected) {
|
if (cursorModel.isPeerControlProtected) {
|
||||||
lastMousePos = ui.Offset(x, y);
|
lastMousePos = ui.Offset(x, y);
|
||||||
|
|||||||
@@ -117,10 +117,11 @@ String prepareTerminalInputPayload(
|
|||||||
|
|
||||||
/// Returns true when a hardware paste shortcut must bypass keyboard modifiers.
|
/// Returns true when a hardware paste shortcut must bypass keyboard modifiers.
|
||||||
///
|
///
|
||||||
/// xterm already handles hardware Ctrl/Cmd+V correctly in the common case. Only
|
/// xterm already handles each platform's paste shortcut in the common case.
|
||||||
/// intercept while a virtual Ctrl/Alt lock is active, because xterm can emit a
|
/// Only intercept while a virtual Ctrl/Alt lock is active, because xterm can
|
||||||
/// one-character paste as normal text when bracketed paste mode is disabled.
|
/// emit a one-character paste as normal text when bracketed paste mode is off.
|
||||||
bool shouldHandleTerminalPasteShortcut({
|
bool shouldHandleTerminalPasteShortcut({
|
||||||
|
required TargetPlatform platform,
|
||||||
required LogicalKeyboardKey logicalKey,
|
required LogicalKeyboardKey logicalKey,
|
||||||
required bool isKeyDown,
|
required bool isKeyDown,
|
||||||
required bool isKeyRepeat,
|
required bool isKeyRepeat,
|
||||||
@@ -133,8 +134,18 @@ bool shouldHandleTerminalPasteShortcut({
|
|||||||
if (!modifierLockActive) return false;
|
if (!modifierLockActive) return false;
|
||||||
if (!isKeyDown && !isKeyRepeat) return false;
|
if (!isKeyDown && !isKeyRepeat) return false;
|
||||||
if (logicalKey != LogicalKeyboardKey.keyV) return false;
|
if (logicalKey != LogicalKeyboardKey.keyV) return false;
|
||||||
if (altPressed || shiftPressed) return false;
|
if (altPressed) return false;
|
||||||
return controlPressed != metaPressed;
|
switch (platform) {
|
||||||
|
case TargetPlatform.linux:
|
||||||
|
return controlPressed && !metaPressed && shiftPressed;
|
||||||
|
case TargetPlatform.iOS:
|
||||||
|
case TargetPlatform.macOS:
|
||||||
|
return !controlPressed && metaPressed && !shiftPressed;
|
||||||
|
case TargetPlatform.android:
|
||||||
|
case TargetPlatform.fuchsia:
|
||||||
|
case TargetPlatform.windows:
|
||||||
|
return controlPressed && !metaPressed && !shiftPressed;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns true when collapsing Row3 should also clear hidden modifier state.
|
/// Returns true when collapsing Row3 should also clear hidden modifier state.
|
||||||
|
|||||||
@@ -25,9 +25,6 @@ import 'package:flutter_hbb/models/user_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/models/desktop_render_texture.dart';
|
||||||
import 'package:flutter_hbb/models/terminal_model.dart';
|
import 'package:flutter_hbb/models/terminal_model.dart';
|
||||||
import 'package:flutter_hbb/plugin/event.dart';
|
|
||||||
import 'package:flutter_hbb/plugin/manager.dart';
|
|
||||||
import 'package:flutter_hbb/plugin/widgets/desc_ui.dart';
|
|
||||||
import 'package:flutter_hbb/common/shared_state.dart';
|
import 'package:flutter_hbb/common/shared_state.dart';
|
||||||
import 'package:flutter_hbb/utils/multi_window_manager.dart';
|
import 'package:flutter_hbb/utils/multi_window_manager.dart';
|
||||||
import 'package:flutter_hbb/utils/http_service.dart' as http;
|
import 'package:flutter_hbb/utils/http_service.dart' as http;
|
||||||
@@ -127,6 +124,8 @@ class FfiModel with ChangeNotifier {
|
|||||||
Timer? _restartReconnectDelayTimer;
|
Timer? _restartReconnectDelayTimer;
|
||||||
var _reconnects = 1;
|
var _reconnects = 1;
|
||||||
DateTime? _offlineReconnectStartTime;
|
DateTime? _offlineReconnectStartTime;
|
||||||
|
bool _androidDocumentPickerActive = false;
|
||||||
|
bool _androidDocumentPickerInterruptedConnection = false;
|
||||||
bool _viewOnly = false;
|
bool _viewOnly = false;
|
||||||
bool _showMyCursor = false;
|
bool _showMyCursor = false;
|
||||||
WeakReference<FFI> parent;
|
WeakReference<FFI> parent;
|
||||||
@@ -258,6 +257,8 @@ class FfiModel with ChangeNotifier {
|
|||||||
_inputBlocked = false;
|
_inputBlocked = false;
|
||||||
_timer?.cancel();
|
_timer?.cancel();
|
||||||
_timer = null;
|
_timer = null;
|
||||||
|
_androidDocumentPickerActive = false;
|
||||||
|
_androidDocumentPickerInterruptedConnection = false;
|
||||||
resetRestartReconnectState();
|
resetRestartReconnectState();
|
||||||
clearPermissions();
|
clearPermissions();
|
||||||
waitForImageTimer?.cancel();
|
waitForImageTimer?.cancel();
|
||||||
@@ -437,15 +438,6 @@ class FfiModel with ChangeNotifier {
|
|||||||
parent.target?.serverModel.updateVoiceCallState(evt);
|
parent.target?.serverModel.updateVoiceCallState(evt);
|
||||||
} else if (name == 'fingerprint') {
|
} else if (name == 'fingerprint') {
|
||||||
FingerprintState.find(peerId).value = evt['fingerprint'] ?? '';
|
FingerprintState.find(peerId).value = evt['fingerprint'] ?? '';
|
||||||
} else if (name == 'plugin_manager') {
|
|
||||||
pluginManager.handleEvent(evt);
|
|
||||||
} else if (name == 'plugin_event') {
|
|
||||||
handlePluginEvent(evt,
|
|
||||||
(Map<String, dynamic> e) => handleMsgBox(e, sessionId, peerId));
|
|
||||||
} else if (name == 'plugin_reload') {
|
|
||||||
handleReloading(evt);
|
|
||||||
} else if (name == 'plugin_option') {
|
|
||||||
handleOption(evt);
|
|
||||||
} else if (name == "sync_peer_hash_password_to_personal_ab") {
|
} else if (name == "sync_peer_hash_password_to_personal_ab") {
|
||||||
if (desktopType == DesktopType.main || isWeb || isMobile) {
|
if (desktopType == DesktopType.main || isWeb || isMobile) {
|
||||||
final id = evt['id'];
|
final id = evt['id'];
|
||||||
@@ -735,11 +727,6 @@ class FfiModel with ChangeNotifier {
|
|||||||
_handleUseTextureRender(
|
_handleUseTextureRender(
|
||||||
Map<String, dynamic> evt, SessionID sessionId, String peerId) {
|
Map<String, dynamic> evt, SessionID sessionId, String peerId) {
|
||||||
parent.target?.imageModel.setUseTextureRender(evt['v'] == 'Y');
|
parent.target?.imageModel.setUseTextureRender(evt['v'] == 'Y');
|
||||||
if (evt['reason'] == 'fallback') {
|
|
||||||
// The Rust watchdog detected that pushed frames were never rendered
|
|
||||||
// and switched this session to software rendering.
|
|
||||||
showToast(translate('texture-render-fallback-tip'));
|
|
||||||
}
|
|
||||||
waitForFirstImage.value = true;
|
waitForFirstImage.value = true;
|
||||||
isRefreshing = true;
|
isRefreshing = true;
|
||||||
showConnectedWaitingForImage(parent.target!.dialogManager, sessionId,
|
showConnectedWaitingForImage(parent.target!.dialogManager, sessionId,
|
||||||
@@ -909,6 +896,13 @@ class FfiModel with ChangeNotifier {
|
|||||||
final text = evt['text'];
|
final text = evt['text'];
|
||||||
final link = evt['link'];
|
final link = evt['link'];
|
||||||
|
|
||||||
|
if (isAndroid &&
|
||||||
|
_androidDocumentPickerActive &&
|
||||||
|
title == 'Connection Error') {
|
||||||
|
_androidDocumentPickerInterruptedConnection = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Disable relative mouse mode on any error-type message to ensure cursor is released.
|
// Disable relative mouse mode on any error-type message to ensure cursor is released.
|
||||||
// This includes connection errors, session-ending messages, elevation errors, etc.
|
// This includes connection errors, session-ending messages, elevation errors, etc.
|
||||||
// Safety: releasing pointer lock on errors prevents the user from being stuck.
|
// Safety: releasing pointer lock on errors prevents the user from being stuck.
|
||||||
@@ -925,17 +919,12 @@ class FfiModel with ChangeNotifier {
|
|||||||
enter2FaDialog(sessionId, dialogManager);
|
enter2FaDialog(sessionId, dialogManager);
|
||||||
} else if (type == 'input-password') {
|
} else if (type == 'input-password') {
|
||||||
enterPasswordDialog(sessionId, dialogManager);
|
enterPasswordDialog(sessionId, dialogManager);
|
||||||
} else if (type == 'session-login' || type == 'session-re-login') {
|
|
||||||
enterUserLoginDialog(sessionId, dialogManager, 'login_linux_tip', true);
|
|
||||||
} else if (type == 'session-login-password') {
|
|
||||||
enterUserLoginAndPasswordDialog(
|
|
||||||
sessionId, dialogManager, 'login_linux_tip', true);
|
|
||||||
} else if (type == 'terminal-admin-login') {
|
} else if (type == 'terminal-admin-login') {
|
||||||
enterUserLoginDialog(
|
enterUserLoginDialog(
|
||||||
sessionId, dialogManager, 'terminal-admin-login-tip', false);
|
sessionId, dialogManager, 'terminal-admin-login-tip');
|
||||||
} else if (type == 'terminal-admin-login-password') {
|
} else if (type == 'terminal-admin-login-password') {
|
||||||
enterUserLoginAndPasswordDialog(
|
enterUserLoginAndPasswordDialog(
|
||||||
sessionId, dialogManager, 'terminal-admin-login-tip', false);
|
sessionId, dialogManager, 'terminal-admin-login-tip');
|
||||||
} else if (type == 'restarting') {
|
} else if (type == 'restarting') {
|
||||||
// Treat restart messages as reconnect control events. Rust still sends
|
// Treat restart messages as reconnect control events. Rust still sends
|
||||||
// title/text for legacy UI and translation reuse; Flutter keeps the last
|
// title/text for legacy UI and translation reuse; Flutter keeps the last
|
||||||
@@ -990,6 +979,23 @@ class FfiModel with ChangeNotifier {
|
|||||||
_restartReconnectDelayTimer = null;
|
_restartReconnectDelayTimer = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void beginAndroidDocumentPicker() {
|
||||||
|
if (!isAndroid) return;
|
||||||
|
_androidDocumentPickerActive = true;
|
||||||
|
_androidDocumentPickerInterruptedConnection = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void endAndroidDocumentPicker() {
|
||||||
|
if (!isAndroid) return;
|
||||||
|
_androidDocumentPickerActive = false;
|
||||||
|
if (!_androidDocumentPickerInterruptedConnection ||
|
||||||
|
parent.target?.closed == true) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_androidDocumentPickerInterruptedConnection = false;
|
||||||
|
reconnect(parent.target!.dialogManager, sessionId, false);
|
||||||
|
}
|
||||||
|
|
||||||
/// Auto-retry check for "Remote desktop is offline" error.
|
/// Auto-retry check for "Remote desktop is offline" error.
|
||||||
/// returns true to auto-retry, false otherwise.
|
/// returns true to auto-retry, false otherwise.
|
||||||
bool shouldAutoRetryOnOffline(
|
bool shouldAutoRetryOnOffline(
|
||||||
@@ -2230,6 +2236,7 @@ class CanvasModel with ChangeNotifier {
|
|||||||
double _y = 0;
|
double _y = 0;
|
||||||
// image scale
|
// image scale
|
||||||
double _scale = 1.0;
|
double _scale = 1.0;
|
||||||
|
bool _locked = false;
|
||||||
double _devicePixelRatio = 1.0;
|
double _devicePixelRatio = 1.0;
|
||||||
Size _size = Size.zero;
|
Size _size = Size.zero;
|
||||||
// the tabbar over the image
|
// the tabbar over the image
|
||||||
@@ -2278,12 +2285,19 @@ class CanvasModel with ChangeNotifier {
|
|||||||
double get x => _x;
|
double get x => _x;
|
||||||
double get y => _y;
|
double get y => _y;
|
||||||
double get scale => _scale;
|
double get scale => _scale;
|
||||||
|
bool get locked => _locked;
|
||||||
double get devicePixelRatio => _devicePixelRatio;
|
double get devicePixelRatio => _devicePixelRatio;
|
||||||
Size get size => _size;
|
Size get size => _size;
|
||||||
ScrollStyle get scrollStyle => _scrollStyle;
|
ScrollStyle get scrollStyle => _scrollStyle;
|
||||||
ViewStyle get viewStyle => _lastViewStyle;
|
ViewStyle get viewStyle => _lastViewStyle;
|
||||||
RxBool get imageOverflow => _imageOverflow;
|
RxBool get imageOverflow => _imageOverflow;
|
||||||
|
|
||||||
|
void setLocked(bool value) {
|
||||||
|
if (_locked == value) return;
|
||||||
|
_locked = value;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
_resetScroll() => setScrollPercent(0.0, 0.0);
|
_resetScroll() => setScrollPercent(0.0, 0.0);
|
||||||
|
|
||||||
void setScrollPercent(double x, double y) {
|
void setScrollPercent(double x, double y) {
|
||||||
@@ -2512,6 +2526,7 @@ class CanvasModel with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void updateLocalCursor(double x, double y) {
|
void updateLocalCursor(double x, double y) {
|
||||||
|
if (parent.target?.ffiModel.viewOnly == true) return;
|
||||||
// If keyboard is not permitted, do not move cursor when mouse is moving.
|
// If keyboard is not permitted, do not move cursor when mouse is moving.
|
||||||
if (parent.target != null && parent.target!.ffiModel.keyboard) {
|
if (parent.target != null && parent.target!.ffiModel.keyboard) {
|
||||||
// Draw cursor if is not desktop.
|
// Draw cursor if is not desktop.
|
||||||
@@ -2744,6 +2759,7 @@ class CanvasModel with ChangeNotifier {
|
|||||||
_x = 0;
|
_x = 0;
|
||||||
_y = 0;
|
_y = 0;
|
||||||
_scale = 1.0;
|
_scale = 1.0;
|
||||||
|
_locked = false;
|
||||||
_lastViewStyle = ViewStyle.defaultViewStyle();
|
_lastViewStyle = ViewStyle.defaultViewStyle();
|
||||||
_timerMobileFocusCanvasCursor?.cancel();
|
_timerMobileFocusCanvasCursor?.cancel();
|
||||||
_timerMobileRestoreCanvasOffset?.cancel();
|
_timerMobileRestoreCanvasOffset?.cancel();
|
||||||
@@ -2855,7 +2871,7 @@ class CursorData {
|
|||||||
required this.width,
|
required this.width,
|
||||||
required this.height,
|
required this.height,
|
||||||
}) : hotx = hotxOrigin * scale,
|
}) : hotx = hotxOrigin * scale,
|
||||||
hoty = hotxOrigin * scale;
|
hoty = hotyOrigin * scale;
|
||||||
|
|
||||||
int _doubleToInt(double v) => (v * 10e6).round().toInt();
|
int _doubleToInt(double v) => (v * 10e6).round().toInt();
|
||||||
|
|
||||||
@@ -3581,6 +3597,16 @@ class QualityMonitorModel with ChangeNotifier {
|
|||||||
bool get show => _show;
|
bool get show => _show;
|
||||||
QualityMonitorData get data => _data;
|
QualityMonitorData get data => _data;
|
||||||
|
|
||||||
|
// Only a WebRTC session names its transport here: web has no session tab
|
||||||
|
// to show it on, and WebRTC is the one path that can be direct or TURN.
|
||||||
|
String? get webrtcTransport {
|
||||||
|
final ffiModel = parent.target?.ffiModel;
|
||||||
|
if (ffiModel == null) return null;
|
||||||
|
final streamType = ffiModel.cachedPeerData.streamType;
|
||||||
|
if (!streamType.startsWith('WebRTC')) return null;
|
||||||
|
return ffiModel.direct == false ? '$streamType (TURN)' : streamType;
|
||||||
|
}
|
||||||
|
|
||||||
checkShowQualityMonitor(SessionID sessionId) async {
|
checkShowQualityMonitor(SessionID sessionId) async {
|
||||||
final show = await bind.sessionGetToggleOption(
|
final show = await bind.sessionGetToggleOption(
|
||||||
sessionId: sessionId, arg: 'show-quality-monitor') ==
|
sessionId: sessionId, arg: 'show-quality-monitor') ==
|
||||||
@@ -4072,6 +4098,11 @@ class FFI {
|
|||||||
return await platformFFI.invokeMethod(method, arguments);
|
return await platformFFI.invokeMethod(method, arguments);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<T?> invokeMethodWithResult<T>(String method,
|
||||||
|
[dynamic arguments]) async {
|
||||||
|
return await platformFFI.invokeMethodWithResult<T>(method, arguments);
|
||||||
|
}
|
||||||
|
|
||||||
// Terminal model management
|
// Terminal model management
|
||||||
void registerTerminalModel(int terminalId, TerminalModel model) {
|
void registerTerminalModel(int terminalId, TerminalModel model) {
|
||||||
debugPrint('[FFI] Registering terminal model for terminal $terminalId');
|
debugPrint('[FFI] Registering terminal model for terminal $terminalId');
|
||||||
@@ -4176,7 +4207,6 @@ class PeerInfo with ChangeNotifier {
|
|||||||
RxBool isSet = false.obs;
|
RxBool isSet = false.obs;
|
||||||
|
|
||||||
bool get isWayland => platformAdditions[kPlatformAdditionsIsWayland] == true;
|
bool get isWayland => platformAdditions[kPlatformAdditionsIsWayland] == true;
|
||||||
bool get isHeadless => platformAdditions[kPlatformAdditionsHeadless] == true;
|
|
||||||
bool get isInstalled =>
|
bool get isInstalled =>
|
||||||
platform != kPeerPlatformWindows ||
|
platform != kPeerPlatformWindows ||
|
||||||
platformAdditions[kPlatformAdditionsIsInstalled] == true;
|
platformAdditions[kPlatformAdditionsIsInstalled] == true;
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import 'dart:io';
|
|||||||
import 'dart:ui' as ui;
|
import 'dart:ui' as ui;
|
||||||
|
|
||||||
import 'package:device_info_plus/device_info_plus.dart';
|
import 'package:device_info_plus/device_info_plus.dart';
|
||||||
import 'package:external_path/external_path.dart';
|
|
||||||
import 'package:ffi/ffi.dart';
|
import 'package:ffi/ffi.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
@@ -131,12 +130,6 @@ class PlatformFFI {
|
|||||||
void registerGpuTexture(SessionID sessionId, int display, int ptr) =>
|
void registerGpuTexture(SessionID sessionId, int display, int ptr) =>
|
||||||
_ffiBind.sessionRegisterGpuTexture(
|
_ffiBind.sessionRegisterGpuTexture(
|
||||||
sessionId: sessionId, display: display, ptr: ptr);
|
sessionId: sessionId, display: display, ptr: ptr);
|
||||||
void unregisterPixelbufferTexture(SessionID sessionId, int display, int ptr) =>
|
|
||||||
_ffiBind.sessionUnregisterPixelbufferTexture(
|
|
||||||
sessionId: sessionId, display: display, ptr: ptr);
|
|
||||||
void unregisterGpuTexture(SessionID sessionId, int display, int ptr) =>
|
|
||||||
_ffiBind.sessionUnregisterGpuTexture(
|
|
||||||
sessionId: sessionId, display: display, ptr: ptr);
|
|
||||||
|
|
||||||
/// Init the FFI class, loads the native Rust core library.
|
/// Init the FFI class, loads the native Rust core library.
|
||||||
Future<void> init(String appType) async {
|
Future<void> init(String appType) async {
|
||||||
@@ -177,8 +170,10 @@ class PlatformFFI {
|
|||||||
_startListenEvent(_ffiBind); // global event
|
_startListenEvent(_ffiBind); // global event
|
||||||
try {
|
try {
|
||||||
if (isAndroid) {
|
if (isAndroid) {
|
||||||
// only support for android
|
// Android file transfer uses app-specific storage. User-selected
|
||||||
_homeDir = (await ExternalPath.getExternalStorageDirectories())[0];
|
// files enter and leave this workspace through the system picker.
|
||||||
|
_homeDir = (await getExternalStorageDirectory())?.path ??
|
||||||
|
(await getApplicationSupportDirectory()).path;
|
||||||
} else if (isIOS) {
|
} else if (isIOS) {
|
||||||
// The previous code was `_homeDir = (await getDownloadsDirectory())?.path ?? '';`,
|
// The previous code was `_homeDir = (await getDownloadsDirectory())?.path ?? '';`,
|
||||||
// which provided the `downloads` path in the sandbox.
|
// which provided the `downloads` path in the sandbox.
|
||||||
@@ -312,6 +307,12 @@ class PlatformFFI {
|
|||||||
return await _toAndroidChannel.invokeMethod(method, arguments);
|
return await _toAndroidChannel.invokeMethod(method, arguments);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<T?> invokeMethodWithResult<T>(String method,
|
||||||
|
[dynamic arguments]) async {
|
||||||
|
if (!isAndroid) return null;
|
||||||
|
return await _toAndroidChannel.invokeMethod<T>(method, arguments);
|
||||||
|
}
|
||||||
|
|
||||||
void syncAndroidServiceAppDirConfigPath() {
|
void syncAndroidServiceAppDirConfigPath() {
|
||||||
invokeMethod(AndroidChannel.kSyncAppDirConfigPath, _dir);
|
invokeMethod(AndroidChannel.kSyncAppDirConfigPath, _dir);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,108 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:xterm/xterm.dart';
|
import 'package:xterm/xterm.dart';
|
||||||
|
|
||||||
|
enum TerminalClipboardWritePermission { denied, unconfigured, allowed }
|
||||||
|
|
||||||
class RustDeskTerminal extends Terminal {
|
class RustDeskTerminal extends Terminal {
|
||||||
RustDeskTerminal({super.maxLines});
|
RustDeskTerminal({
|
||||||
|
super.maxLines,
|
||||||
|
required TerminalClipboardWritePermission Function()
|
||||||
|
clipboardWritePermission,
|
||||||
|
required Future<bool> Function(String) onClipboardWrite,
|
||||||
|
ValueChanged<String>? onClipboardWriteBlocked,
|
||||||
|
ValueChanged<String>? onClipboardWriteSucceeded,
|
||||||
|
}) : _clipboardWritePermission = clipboardWritePermission,
|
||||||
|
_onClipboardWrite = onClipboardWrite,
|
||||||
|
_onClipboardWriteBlocked = onClipboardWriteBlocked,
|
||||||
|
_onClipboardWriteSucceeded = onClipboardWriteSucceeded {
|
||||||
|
onPrivateOSC = _handlePrivateOsc;
|
||||||
|
}
|
||||||
|
|
||||||
|
static const _clipboardOscCode = '52';
|
||||||
|
static const _systemClipboardSelection = 'c';
|
||||||
|
// Match the terminal helper's existing payload safety ceiling.
|
||||||
|
static const _maxClipboardWriteBytes = 16 * 1024 * 1024;
|
||||||
|
static const _base64InputBytesPerBlock = 3;
|
||||||
|
static const _base64EncodedCharsPerBlock = 4;
|
||||||
|
static final _osc52Selection = RegExp(r'^[cpqs0-7]*$');
|
||||||
|
final TerminalClipboardWritePermission Function() _clipboardWritePermission;
|
||||||
|
final Future<bool> Function(String) _onClipboardWrite;
|
||||||
|
final ValueChanged<String>? _onClipboardWriteBlocked;
|
||||||
|
final ValueChanged<String>? _onClipboardWriteSucceeded;
|
||||||
|
|
||||||
|
bool get isClipboardWriteAllowed =>
|
||||||
|
_clipboardWritePermission() == TerminalClipboardWritePermission.allowed;
|
||||||
|
|
||||||
|
void _handlePrivateOsc(String code, List<String> args) {
|
||||||
|
if (code != _clipboardOscCode) return;
|
||||||
|
if (args.length != 2 || !_osc52Selection.hasMatch(args.first)) {
|
||||||
|
debugPrint('[RustDeskTerminal] Rejected malformed OSC 52 command');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (args.last == '?') {
|
||||||
|
debugPrint('[RustDeskTerminal] Rejected OSC 52 clipboard query');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final permission = _clipboardWritePermission();
|
||||||
|
if (permission == TerminalClipboardWritePermission.denied) {
|
||||||
|
debugPrint('[RustDeskTerminal] Rejected unauthorized OSC 52 write');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final selection = args.first;
|
||||||
|
if (selection.isNotEmpty &&
|
||||||
|
!selection.contains(_systemClipboardSelection)) {
|
||||||
|
debugPrint('[RustDeskTerminal] Ignored unsupported OSC 52 selection');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (selection.replaceAll(_systemClipboardSelection, '').isNotEmpty) {
|
||||||
|
debugPrint('[RustDeskTerminal] Ignored unsupported OSC 52 selections');
|
||||||
|
}
|
||||||
|
final text = _decodeClipboardPayload(args.last);
|
||||||
|
if (text == null) return;
|
||||||
|
if (permission == TerminalClipboardWritePermission.unconfigured) {
|
||||||
|
debugPrint('[RustDeskTerminal] Blocked OSC 52 write pending consent');
|
||||||
|
_onClipboardWriteBlocked?.call(text);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
unawaited(_writeClipboard(text));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _writeClipboard(String text) async {
|
||||||
|
final succeeded = await _onClipboardWrite(text);
|
||||||
|
if (succeeded) {
|
||||||
|
_onClipboardWriteSucceeded?.call(text);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
debugPrint(
|
||||||
|
'[RustDeskTerminal] OSC 52 clipboard write requires interaction');
|
||||||
|
_onClipboardWriteBlocked?.call(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
String? _decodeClipboardPayload(String payload) {
|
||||||
|
if (payload.length > _maxBase64EncodedLength(_maxClipboardWriteBytes)) {
|
||||||
|
debugPrint('[RustDeskTerminal] Rejected oversized OSC 52 payload');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
final bytes = base64.decode(payload);
|
||||||
|
if (bytes.length > _maxClipboardWriteBytes) {
|
||||||
|
debugPrint('[RustDeskTerminal] Rejected oversized OSC 52 payload');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return utf8.decode(bytes);
|
||||||
|
} on FormatException {
|
||||||
|
debugPrint('[RustDeskTerminal] Rejected malformed OSC 52 payload');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static int _maxBase64EncodedLength(int maxBytes) =>
|
||||||
|
((maxBytes + _base64InputBytesPerBlock - 1) ~/
|
||||||
|
_base64InputBytesPerBlock) *
|
||||||
|
_base64EncodedCharsPerBlock;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void eraseScrollbackOnly() {
|
void eraseScrollbackOnly() {
|
||||||
|
|||||||
@@ -210,15 +210,10 @@ class ServerModel with ChangeNotifier {
|
|||||||
_audioOk = audioOption != 'N';
|
_audioOk = audioOption != 'N';
|
||||||
}
|
}
|
||||||
|
|
||||||
// file
|
// Android file transfer is confined to app-specific storage. Files enter
|
||||||
if (!await AndroidPermissionManager.check(kManageExternalStorage)) {
|
// and leave the workspace through Android's system document picker.
|
||||||
_fileOk = false;
|
final fileOption = await bind.mainGetOption(key: kOptionEnableFileTransfer);
|
||||||
bind.mainSetOption(key: kOptionEnableFileTransfer, value: "N");
|
_fileOk = fileOption != 'N';
|
||||||
} else {
|
|
||||||
final fileOption =
|
|
||||||
await bind.mainGetOption(key: kOptionEnableFileTransfer);
|
|
||||||
_fileOk = fileOption != 'N';
|
|
||||||
}
|
|
||||||
|
|
||||||
// clipboard
|
// clipboard
|
||||||
final clipOption = await bind.mainGetOption(key: kOptionEnableClipboard);
|
final clipOption = await bind.mainGetOption(key: kOptionEnableClipboard);
|
||||||
@@ -319,16 +314,6 @@ class ServerModel with ChangeNotifier {
|
|||||||
if (clients.any((c) => !c.disconnected)) {
|
if (clients.any((c) => !c.disconnected)) {
|
||||||
await showClientsMayNotBeChangedAlert(parent.target);
|
await showClientsMayNotBeChangedAlert(parent.target);
|
||||||
}
|
}
|
||||||
if (!_fileOk &&
|
|
||||||
!await AndroidPermissionManager.check(kManageExternalStorage)) {
|
|
||||||
final res =
|
|
||||||
await AndroidPermissionManager.request(kManageExternalStorage);
|
|
||||||
if (!res) {
|
|
||||||
showToast(translate('Failed'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_fileOk = !_fileOk;
|
_fileOk = !_fileOk;
|
||||||
bind.mainSetOption(
|
bind.mainSetOption(
|
||||||
key: kOptionEnableFileTransfer,
|
key: kOptionEnableFileTransfer,
|
||||||
@@ -418,9 +403,6 @@ class ServerModel with ChangeNotifier {
|
|||||||
if (bind.mainGetLocalOption(key: kOptionDisableFloatingWindow) != 'Y') {
|
if (bind.mainGetLocalOption(key: kOptionDisableFloatingWindow) != 'Y') {
|
||||||
await checkFloatingWindowPermission();
|
await checkFloatingWindowPermission();
|
||||||
}
|
}
|
||||||
if (!await AndroidPermissionManager.check(kManageExternalStorage)) {
|
|
||||||
await AndroidPermissionManager.request(kManageExternalStorage);
|
|
||||||
}
|
|
||||||
final res = await parent.target?.dialogManager
|
final res = await parent.target?.dialogManager
|
||||||
.show<bool>((setState, close, context) {
|
.show<bool>((setState, close, context) {
|
||||||
submit() => close(true);
|
submit() => close(true);
|
||||||
@@ -738,9 +720,13 @@ class ServerModel with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> closeAll() async {
|
/// `byOperator` false means the CM's window went away rather than a person asking for the
|
||||||
await Future.wait(
|
/// peers to go. The sessions end either way; only the close reason differs, and with it
|
||||||
_clients.map((client) => bind.cmCloseConnection(connId: client.id)));
|
/// whether the peer is allowed to reconnect. See `ipc::Data::CmWindowClosed`.
|
||||||
|
Future<void> closeAll({bool byOperator = true}) async {
|
||||||
|
await Future.wait(_clients.map((client) => byOperator
|
||||||
|
? bind.cmCloseConnection(connId: client.id)
|
||||||
|
: bind.cmCloseConnectionWindow(connId: client.id)));
|
||||||
_clients.clear();
|
_clients.clear();
|
||||||
tabController.state.value.tabs.clear();
|
tabController.state.value.tabs.clear();
|
||||||
if (isAndroid) androidUpdatekeepScreenOn();
|
if (isAndroid) androidUpdatekeepScreenOn();
|
||||||
|
|||||||
15
flutter/lib/models/terminal_clipboard_writer.dart
Normal file
15
flutter/lib/models/terminal_clipboard_writer.dart
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
|
Future<bool> writeTerminalClipboardPlatform(
|
||||||
|
String text, {
|
||||||
|
bool userInitiated = false,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
await Clipboard.setData(ClipboardData(text: text));
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
debugPrint('[Terminal] Failed to write clipboard: $error');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
29
flutter/lib/models/terminal_clipboard_writer_web.dart
Normal file
29
flutter/lib/models/terminal_clipboard_writer_web.dart
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import 'dart:js_interop';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
|
const _writeTerminalClipboardCommand = 'write_terminal_clipboard';
|
||||||
|
|
||||||
|
@JS('setByName')
|
||||||
|
external JSPromise<JSBoolean> _setByName(
|
||||||
|
JSString name,
|
||||||
|
JSString value,
|
||||||
|
JSBoolean userInitiated,
|
||||||
|
);
|
||||||
|
|
||||||
|
Future<bool> writeTerminalClipboardPlatform(
|
||||||
|
String text, {
|
||||||
|
bool userInitiated = false,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
final result = await _setByName(
|
||||||
|
_writeTerminalClipboardCommand.toJS,
|
||||||
|
text.toJS,
|
||||||
|
userInitiated.toJS,
|
||||||
|
).toDart;
|
||||||
|
return result.toDart;
|
||||||
|
} catch (error) {
|
||||||
|
debugPrint('[Terminal] Failed to write Web clipboard: $error');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
201
flutter/lib/models/terminal_copy_shortcut.dart
Normal file
201
flutter/lib/models/terminal_copy_shortcut.dart
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:flutter_hbb/consts.dart';
|
||||||
|
import 'package:xterm/xterm.dart';
|
||||||
|
|
||||||
|
import 'terminal_clipboard_writer.dart'
|
||||||
|
if (dart.library.html) 'terminal_clipboard_writer_web.dart';
|
||||||
|
|
||||||
|
const _controlShiftVPasteShortcut = SingleActivator(
|
||||||
|
LogicalKeyboardKey.keyV,
|
||||||
|
control: true,
|
||||||
|
shift: true,
|
||||||
|
);
|
||||||
|
|
||||||
|
typedef TerminalClipboardWriter = Future<bool> Function(
|
||||||
|
String text, {
|
||||||
|
required bool userInitiated,
|
||||||
|
});
|
||||||
|
|
||||||
|
class TerminalClipboardNoticeRequest<T> {
|
||||||
|
const TerminalClipboardNoticeRequest({
|
||||||
|
required this.source,
|
||||||
|
required this.text,
|
||||||
|
required this.persistAllowed,
|
||||||
|
});
|
||||||
|
|
||||||
|
final T source;
|
||||||
|
final String text;
|
||||||
|
final bool persistAllowed;
|
||||||
|
|
||||||
|
String get actionKey => persistAllowed ? 'Enable' : 'Copy to clipboard';
|
||||||
|
|
||||||
|
String get negativeActionKey => persistAllowed ? 'Decline' : 'Dismiss';
|
||||||
|
}
|
||||||
|
|
||||||
|
const kTerminalClipboardNoticeMessageKey = 'terminal-clipboard-write-tip';
|
||||||
|
|
||||||
|
class TerminalClipboardNoticeCoordinator<T> extends ChangeNotifier {
|
||||||
|
TerminalClipboardNoticeRequest<T>? _current;
|
||||||
|
bool _noticeVisible = false;
|
||||||
|
bool _actionInProgress = false;
|
||||||
|
|
||||||
|
TerminalClipboardNoticeRequest<T>? get current => _current;
|
||||||
|
bool get canClaimAction =>
|
||||||
|
_noticeVisible && !_actionInProgress && _current != null;
|
||||||
|
|
||||||
|
TerminalClipboardNoticeRequest<T>? currentForSource(T source) {
|
||||||
|
final current = _current;
|
||||||
|
if (current == null || current.source != source) return null;
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
|
||||||
|
TerminalClipboardNoticeRequest<T>? recordBlocked({
|
||||||
|
required T source,
|
||||||
|
required String text,
|
||||||
|
required String option,
|
||||||
|
required bool Function(T source) canWrite,
|
||||||
|
}) {
|
||||||
|
if (!canWrite(source)) return null;
|
||||||
|
final requestAllowsPersistence =
|
||||||
|
option == kTerminalClipboardWriteUnconfigured;
|
||||||
|
if (option != kTerminalClipboardWriteAllowed && !requestAllowsPersistence) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (_noticeVisible && _actionInProgress) return null;
|
||||||
|
final wasVisible = _noticeVisible;
|
||||||
|
final persistAllowed =
|
||||||
|
wasVisible ? _current?.persistAllowed : requestAllowsPersistence;
|
||||||
|
final request = TerminalClipboardNoticeRequest(
|
||||||
|
source: source,
|
||||||
|
text: text,
|
||||||
|
persistAllowed: persistAllowed ?? requestAllowsPersistence,
|
||||||
|
);
|
||||||
|
_current = request;
|
||||||
|
if (wasVisible) return null;
|
||||||
|
_noticeVisible = true;
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
|
TerminalClipboardNoticeRequest<T>? claimCurrentAction() {
|
||||||
|
if (!canClaimAction) return null;
|
||||||
|
final current = _current;
|
||||||
|
if (current == null) return null;
|
||||||
|
_actionInProgress = true;
|
||||||
|
notifyListeners();
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
|
||||||
|
void releaseAction() {
|
||||||
|
if (!_actionInProgress) return;
|
||||||
|
_actionInProgress = false;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool beginClose() {
|
||||||
|
if (!_noticeVisible) return false;
|
||||||
|
_actionInProgress = true;
|
||||||
|
notifyListeners();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void noticeClosed() => clear();
|
||||||
|
|
||||||
|
void clear() {
|
||||||
|
_current = null;
|
||||||
|
_noticeVisible = false;
|
||||||
|
_actionInProgress = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> writeTerminalClipboard(
|
||||||
|
String text, {
|
||||||
|
bool userInitiated = false,
|
||||||
|
}) =>
|
||||||
|
writeTerminalClipboardPlatform(text, userInitiated: userInitiated);
|
||||||
|
|
||||||
|
Future<bool> completeTerminalClipboardWrite({
|
||||||
|
required String clipboardText,
|
||||||
|
required bool Function() canWrite,
|
||||||
|
required TerminalClipboardWriter writeClipboard,
|
||||||
|
Future<void> Function()? persistAllowed,
|
||||||
|
}) async {
|
||||||
|
if (!canWrite()) return false;
|
||||||
|
if (!await writeClipboard(clipboardText, userInitiated: true)) return false;
|
||||||
|
await persistAllowed?.call();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<ShortcutActivator, Intent>? platformTerminalShortcuts() {
|
||||||
|
final platform = defaultTargetPlatform;
|
||||||
|
if (platform == TargetPlatform.linux) {
|
||||||
|
return {
|
||||||
|
for (final entry in defaultTerminalShortcuts.entries)
|
||||||
|
if (!_isControlShortcut(entry.key, LogicalKeyboardKey.keyV))
|
||||||
|
entry.key: entry.value,
|
||||||
|
_controlShiftVPasteShortcut:
|
||||||
|
const PasteTextIntent(SelectionChangedCause.keyboard),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (platform != TargetPlatform.windows &&
|
||||||
|
platform != TargetPlatform.android) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
for (final entry in defaultTerminalShortcuts.entries)
|
||||||
|
if (!_isControlShortcut(
|
||||||
|
entry.key,
|
||||||
|
LogicalKeyboardKey.keyC,
|
||||||
|
shift: true,
|
||||||
|
))
|
||||||
|
entry.key: entry.value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _isControlShortcut(
|
||||||
|
ShortcutActivator shortcut,
|
||||||
|
LogicalKeyboardKey key, {
|
||||||
|
bool shift = false,
|
||||||
|
}) =>
|
||||||
|
shortcut is SingleActivator &&
|
||||||
|
shortcut.trigger == key &&
|
||||||
|
shortcut.control &&
|
||||||
|
shortcut.shift == shift &&
|
||||||
|
!shortcut.alt &&
|
||||||
|
!shortcut.meta;
|
||||||
|
|
||||||
|
FocusOnKeyEventCallback terminalCopyHandler(
|
||||||
|
Terminal terminal,
|
||||||
|
TerminalController controller, {
|
||||||
|
FocusOnKeyEventCallback? fallback,
|
||||||
|
}) =>
|
||||||
|
(focusNode, event) {
|
||||||
|
if (_isSelectionCopyShortcut(event)) {
|
||||||
|
final selection = controller.selection;
|
||||||
|
if (selection != null && !selection.isCollapsed) {
|
||||||
|
if (event is KeyDownEvent) {
|
||||||
|
final text = terminal.buffer.getText(selection);
|
||||||
|
unawaited(writeTerminalClipboard(text, userInitiated: true));
|
||||||
|
}
|
||||||
|
return KeyEventResult.handled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fallback?.call(focusNode, event) ?? KeyEventResult.ignored;
|
||||||
|
};
|
||||||
|
|
||||||
|
bool _isSelectionCopyShortcut(KeyEvent event) {
|
||||||
|
final keyboard = HardwareKeyboard.instance;
|
||||||
|
final platform = defaultTargetPlatform;
|
||||||
|
final usesControlCopy =
|
||||||
|
platform == TargetPlatform.windows || platform == TargetPlatform.android;
|
||||||
|
return usesControlCopy &&
|
||||||
|
(event is KeyDownEvent || event is KeyRepeatEvent) &&
|
||||||
|
event.logicalKey == LogicalKeyboardKey.keyC &&
|
||||||
|
keyboard.isControlPressed &&
|
||||||
|
!keyboard.isShiftPressed &&
|
||||||
|
!keyboard.isAltPressed &&
|
||||||
|
!keyboard.isMetaPressed;
|
||||||
|
}
|
||||||
@@ -11,8 +11,38 @@ import 'input_modifier_utils.dart';
|
|||||||
import 'model.dart';
|
import 'model.dart';
|
||||||
import 'platform_model.dart';
|
import 'platform_model.dart';
|
||||||
import 'rustdesk_terminal.dart';
|
import 'rustdesk_terminal.dart';
|
||||||
|
import 'terminal_copy_shortcut.dart';
|
||||||
import 'terminal_mouse_handler.dart';
|
import 'terminal_mouse_handler.dart';
|
||||||
|
|
||||||
|
bool canConfigureTerminalClipboardPermission({
|
||||||
|
required bool settingsDisabled,
|
||||||
|
required bool optionFixed,
|
||||||
|
}) =>
|
||||||
|
!settingsDisabled && !optionFixed;
|
||||||
|
|
||||||
|
bool canHandleTerminalClipboardWriteRequest({
|
||||||
|
required String localOption,
|
||||||
|
required bool canConfigurePermission,
|
||||||
|
}) =>
|
||||||
|
canConfigurePermission || localOption == kTerminalClipboardWriteAllowed;
|
||||||
|
|
||||||
|
TerminalClipboardWritePermission terminalClipboardWritePermission(
|
||||||
|
String localOption, {
|
||||||
|
required bool remoteClipboardEnabled,
|
||||||
|
bool canRequestConsent = true,
|
||||||
|
}) {
|
||||||
|
if (!remoteClipboardEnabled) {
|
||||||
|
return TerminalClipboardWritePermission.denied;
|
||||||
|
}
|
||||||
|
if (localOption == kTerminalClipboardWriteAllowed) {
|
||||||
|
return TerminalClipboardWritePermission.allowed;
|
||||||
|
}
|
||||||
|
if (localOption == kTerminalClipboardWriteUnconfigured && canRequestConsent) {
|
||||||
|
return TerminalClipboardWritePermission.unconfigured;
|
||||||
|
}
|
||||||
|
return TerminalClipboardWritePermission.denied;
|
||||||
|
}
|
||||||
|
|
||||||
class TerminalModel with ChangeNotifier {
|
class TerminalModel with ChangeNotifier {
|
||||||
final String id; // peer id
|
final String id; // peer id
|
||||||
final FFI parent;
|
final FFI parent;
|
||||||
@@ -62,6 +92,9 @@ class TerminalModel with ChangeNotifier {
|
|||||||
/// The listener (typically TerminalPage) can use this to auto-close the tab/page.
|
/// The listener (typically TerminalPage) can use this to auto-close the tab/page.
|
||||||
VoidCallback? onClosed;
|
VoidCallback? onClosed;
|
||||||
|
|
||||||
|
ValueChanged<String>? onClipboardWriteBlocked;
|
||||||
|
ValueChanged<String>? onClipboardWriteSucceeded;
|
||||||
|
|
||||||
Future<void> _handleInput(String data) async {
|
Future<void> _handleInput(String data) async {
|
||||||
// xterm can complete asynchronous input after the Flutter page has gone
|
// xterm can complete asynchronous input after the Flutter page has gone
|
||||||
// away. Stop before reading or clearing widget-owned modifier state.
|
// away. Stop before reading or clearing widget-owned modifier state.
|
||||||
@@ -130,7 +163,19 @@ class TerminalModel with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
TerminalModel(this.parent, [this.terminalId = 0]) : id = parent.id {
|
TerminalModel(this.parent, [this.terminalId = 0]) : id = parent.id {
|
||||||
terminal = RustDeskTerminal(maxLines: 10000);
|
terminal = RustDeskTerminal(
|
||||||
|
maxLines: 10000,
|
||||||
|
onClipboardWrite: writeTerminalClipboard,
|
||||||
|
clipboardWritePermission: () => terminalClipboardWritePermission(
|
||||||
|
bind.mainGetLocalOption(key: kOptionAllowTerminalClipboardWrite),
|
||||||
|
remoteClipboardEnabled:
|
||||||
|
parent.ffiModel.permissions['clipboard'] != false,
|
||||||
|
canRequestConsent: onClipboardWriteBlocked != null,
|
||||||
|
),
|
||||||
|
onClipboardWriteBlocked: (text) => onClipboardWriteBlocked?.call(text),
|
||||||
|
onClipboardWriteSucceeded: (text) =>
|
||||||
|
onClipboardWriteSucceeded?.call(text),
|
||||||
|
);
|
||||||
terminal.mouseHandler = const WheelButtonFixMouseHandler();
|
terminal.mouseHandler = const WheelButtonFixMouseHandler();
|
||||||
terminalController = TerminalController();
|
terminalController = TerminalController();
|
||||||
|
|
||||||
@@ -593,6 +638,8 @@ class TerminalModel with ChangeNotifier {
|
|||||||
clearAltLock = null;
|
clearAltLock = null;
|
||||||
onResizeExternal = null;
|
onResizeExternal = null;
|
||||||
onClosed = null;
|
onClosed = null;
|
||||||
|
onClipboardWriteBlocked = null;
|
||||||
|
onClipboardWriteSucceeded = null;
|
||||||
// Clear buffers to free memory
|
// Clear buffers to free memory
|
||||||
_inputBuffer.clear();
|
_inputBuffer.clear();
|
||||||
_pendingOutputChunks.clear();
|
_pendingOutputChunks.clear();
|
||||||
|
|||||||
297
flutter/lib/models/terminal_mouse_drag_reporter.dart
Normal file
297
flutter/lib/models/terminal_mouse_drag_reporter.dart
Normal file
@@ -0,0 +1,297 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/gestures.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:xterm/xterm.dart';
|
||||||
|
|
||||||
|
const _cellIndexOffset = 1;
|
||||||
|
const _legacyCodeOffset = 32;
|
||||||
|
const _leftButtonCode = 0;
|
||||||
|
const _motionButtonCode = 32;
|
||||||
|
const _releaseButtonCode = 3;
|
||||||
|
const _shiftModifierCode = 4;
|
||||||
|
const _metaModifierCode = 8;
|
||||||
|
const _controlModifierCode = 16;
|
||||||
|
const _modifierCodeMask =
|
||||||
|
_shiftModifierCode | _metaModifierCode | _controlModifierCode;
|
||||||
|
const _normalCoordinateLimit = 223;
|
||||||
|
const _utfCoordinateLimit = 2015;
|
||||||
|
|
||||||
|
String encodeTerminalMouseReport(
|
||||||
|
MouseReportMode mode,
|
||||||
|
int button,
|
||||||
|
CellOffset position, {
|
||||||
|
bool release = false,
|
||||||
|
}) {
|
||||||
|
final x = position.x + _cellIndexOffset;
|
||||||
|
final y = position.y + _cellIndexOffset;
|
||||||
|
final reportedButton =
|
||||||
|
release ? _releaseButtonCode | (button & _modifierCodeMask) : button;
|
||||||
|
switch (mode) {
|
||||||
|
case MouseReportMode.normal:
|
||||||
|
case MouseReportMode.utf:
|
||||||
|
final limit = mode == MouseReportMode.normal
|
||||||
|
? _normalCoordinateLimit
|
||||||
|
: _utfCoordinateLimit;
|
||||||
|
final encodedButton =
|
||||||
|
String.fromCharCode(_legacyCodeOffset + reportedButton);
|
||||||
|
return '\x1b[M$encodedButton${_legacyCoordinate(x, limit)}'
|
||||||
|
'${_legacyCoordinate(y, limit)}';
|
||||||
|
case MouseReportMode.sgr:
|
||||||
|
final suffix = release ? 'm' : 'M';
|
||||||
|
return '\x1b[<$button;$x;$y$suffix';
|
||||||
|
case MouseReportMode.urxvt:
|
||||||
|
return '\x1b[${_legacyCodeOffset + reportedButton};$x;${y}M';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _legacyCoordinate(int value, int limit) =>
|
||||||
|
value > limit ? '\x00' : String.fromCharCode(_legacyCodeOffset + value);
|
||||||
|
|
||||||
|
int _activeModifierCode() {
|
||||||
|
final keyboard = HardwareKeyboard.instance;
|
||||||
|
return (keyboard.isShiftPressed ? _shiftModifierCode : 0) |
|
||||||
|
(keyboard.isAltPressed ? _metaModifierCode : 0) |
|
||||||
|
(keyboard.isControlPressed ? _controlModifierCode : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
class TerminalMouseDragReporter {
|
||||||
|
int? _pointerId;
|
||||||
|
TerminalController? _controller;
|
||||||
|
late CellOffset _lastReportedPosition;
|
||||||
|
var _ownsControllerSuspension = false;
|
||||||
|
var _releasePending = false;
|
||||||
|
var _reporting = false;
|
||||||
|
var _dragged = false;
|
||||||
|
|
||||||
|
bool handleDown(
|
||||||
|
PointerDownEvent event,
|
||||||
|
Terminal terminal,
|
||||||
|
TerminalViewState? terminalView, {
|
||||||
|
bool reportTouchInput = false,
|
||||||
|
bool deferReport = false,
|
||||||
|
}) {
|
||||||
|
if (!_isPrimaryPointer(event, reportTouchInput) ||
|
||||||
|
!_reportsDrag(terminal.mouseMode)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (terminalView == null || terminalView.widget.readOnly) return false;
|
||||||
|
final controller = terminalView.widget.controller;
|
||||||
|
if (controller == null ||
|
||||||
|
controller.suspendedPointerInputs ||
|
||||||
|
!controller.pointerInput.inputs.contains(PointerInput.tap)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
cancel();
|
||||||
|
_pointerId = event.pointer;
|
||||||
|
_controller = controller;
|
||||||
|
_ownsControllerSuspension = true;
|
||||||
|
_releasePending = !deferReport;
|
||||||
|
_reporting = !deferReport;
|
||||||
|
_dragged = false;
|
||||||
|
controller.setSuspendPointerInput(true);
|
||||||
|
_clearSelection(controller);
|
||||||
|
final position = _cellAt(event, terminalView);
|
||||||
|
_lastReportedPosition = position;
|
||||||
|
if (!deferReport) {
|
||||||
|
terminal.textInput(
|
||||||
|
_report(terminal.mouseReportMode, position),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool activateDeferredDown(Terminal terminal) {
|
||||||
|
if (_pointerId == null ||
|
||||||
|
_controller == null ||
|
||||||
|
_releasePending ||
|
||||||
|
!_reportsDrag(terminal.mouseMode)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
_releasePending = true;
|
||||||
|
_reporting = true;
|
||||||
|
_clearSelection(_controller);
|
||||||
|
terminal.textInput(
|
||||||
|
_report(terminal.mouseReportMode, _lastReportedPosition),
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool handleMove(
|
||||||
|
PointerMoveEvent event,
|
||||||
|
Terminal terminal,
|
||||||
|
TerminalViewState? terminalView, {
|
||||||
|
void Function(bool dragged)? beforeRelease,
|
||||||
|
void Function()? onCancel,
|
||||||
|
}) {
|
||||||
|
if (event.pointer != _pointerId) return false;
|
||||||
|
if (terminalView == null) {
|
||||||
|
onCancel?.call();
|
||||||
|
cancel();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
final reportsDrag = _reportsDrag(terminal.mouseMode);
|
||||||
|
if (!_hasPrimaryButton(event)) {
|
||||||
|
if (_releasePending && reportsDrag) {
|
||||||
|
_finishRelease(
|
||||||
|
event,
|
||||||
|
terminal,
|
||||||
|
terminalView,
|
||||||
|
beforeRelease: beforeRelease,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
onCancel?.call();
|
||||||
|
}
|
||||||
|
cancel();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!_reporting || !reportsDrag) {
|
||||||
|
if (!reportsDrag && _releasePending) {
|
||||||
|
_releasePending = false;
|
||||||
|
onCancel?.call();
|
||||||
|
}
|
||||||
|
_reporting = false;
|
||||||
|
// Keep ownership until the matching end event to suppress local selection.
|
||||||
|
final controller = _controller;
|
||||||
|
scheduleMicrotask(() => _clearSelection(controller));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
final position = _cellAt(event, terminalView);
|
||||||
|
_recordPosition(position);
|
||||||
|
terminal.textInput(
|
||||||
|
_report(terminal.mouseReportMode, position, motion: true),
|
||||||
|
);
|
||||||
|
final controller = _controller;
|
||||||
|
scheduleMicrotask(() => _clearSelection(controller));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool handleEnd(
|
||||||
|
PointerEvent event,
|
||||||
|
Terminal terminal,
|
||||||
|
TerminalViewState? terminalView, {
|
||||||
|
void Function(bool dragged)? beforeRelease,
|
||||||
|
void Function()? onCancel,
|
||||||
|
}) {
|
||||||
|
if (event.pointer != _pointerId) return false;
|
||||||
|
if (terminalView != null &&
|
||||||
|
_releasePending &&
|
||||||
|
_reportsDrag(terminal.mouseMode)) {
|
||||||
|
_finishRelease(
|
||||||
|
event,
|
||||||
|
terminal,
|
||||||
|
terminalView,
|
||||||
|
beforeRelease: beforeRelease,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
onCancel?.call();
|
||||||
|
}
|
||||||
|
_clearSelection(_controller);
|
||||||
|
final controller = _controller;
|
||||||
|
_pointerId = null;
|
||||||
|
// Keep xterm's tap recognizer suspended for this pointer event.
|
||||||
|
scheduleMicrotask(() {
|
||||||
|
if (_pointerId == null && identical(_controller, controller)) {
|
||||||
|
_clearSelection(controller);
|
||||||
|
cancel();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void cancel() {
|
||||||
|
final controller = _controller;
|
||||||
|
if (_ownsControllerSuspension) {
|
||||||
|
controller?.setSuspendPointerInput(false);
|
||||||
|
}
|
||||||
|
_pointerId = null;
|
||||||
|
_controller = null;
|
||||||
|
_ownsControllerSuspension = false;
|
||||||
|
_releasePending = false;
|
||||||
|
_reporting = false;
|
||||||
|
_dragged = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateController(TerminalController controller) {
|
||||||
|
final oldController = _controller;
|
||||||
|
if (_pointerId == null || oldController == null) {
|
||||||
|
cancel();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (identical(oldController, controller)) return;
|
||||||
|
if (_ownsControllerSuspension) {
|
||||||
|
oldController.setSuspendPointerInput(false);
|
||||||
|
}
|
||||||
|
final acceptsPointerInput = !controller.suspendedPointerInputs &&
|
||||||
|
controller.pointerInput.inputs.contains(PointerInput.tap);
|
||||||
|
_controller = controller;
|
||||||
|
_ownsControllerSuspension = acceptsPointerInput;
|
||||||
|
_reporting = _reporting && acceptsPointerInput;
|
||||||
|
if (_ownsControllerSuspension) controller.setSuspendPointerInput(true);
|
||||||
|
_clearSelection(controller);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _reportRelease(Terminal terminal, CellOffset position) {
|
||||||
|
terminal.textInput(
|
||||||
|
_report(
|
||||||
|
terminal.mouseReportMode,
|
||||||
|
position,
|
||||||
|
release: true,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _finishRelease(
|
||||||
|
PointerEvent event,
|
||||||
|
Terminal terminal,
|
||||||
|
TerminalViewState terminalView, {
|
||||||
|
void Function(bool dragged)? beforeRelease,
|
||||||
|
}) {
|
||||||
|
final position =
|
||||||
|
_reporting ? _cellAt(event, terminalView) : _lastReportedPosition;
|
||||||
|
if (_reporting) _recordPosition(position);
|
||||||
|
beforeRelease?.call(_dragged);
|
||||||
|
_reportRelease(terminal, position);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _recordPosition(CellOffset position) {
|
||||||
|
_dragged = _dragged || position != _lastReportedPosition;
|
||||||
|
_lastReportedPosition = position;
|
||||||
|
}
|
||||||
|
|
||||||
|
CellOffset _cellAt(PointerEvent event, TerminalViewState terminalView) {
|
||||||
|
final renderTerminal = terminalView.renderTerminal;
|
||||||
|
return renderTerminal.getCellOffset(
|
||||||
|
renderTerminal.globalToLocal(event.position),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _isPrimaryPointer(PointerEvent event, bool reportTouchInput) =>
|
||||||
|
(event.kind == PointerDeviceKind.mouse ||
|
||||||
|
reportTouchInput && event.kind == PointerDeviceKind.touch) &&
|
||||||
|
_hasPrimaryButton(event);
|
||||||
|
|
||||||
|
bool _hasPrimaryButton(PointerEvent event) =>
|
||||||
|
(event.buttons & kPrimaryButton) == kPrimaryButton;
|
||||||
|
|
||||||
|
bool _reportsDrag(MouseMode mode) =>
|
||||||
|
mode == MouseMode.upDownScrollDrag || mode == MouseMode.upDownScrollMove;
|
||||||
|
|
||||||
|
void _clearSelection(TerminalController? controller) {
|
||||||
|
if (controller == null || controller.selection == null) return;
|
||||||
|
controller.clearSelection();
|
||||||
|
}
|
||||||
|
|
||||||
|
String _report(
|
||||||
|
MouseReportMode mode,
|
||||||
|
CellOffset position, {
|
||||||
|
bool release = false,
|
||||||
|
bool motion = false,
|
||||||
|
}) {
|
||||||
|
final baseButton = motion ? _motionButtonCode : _leftButtonCode;
|
||||||
|
final button = baseButton | _activeModifierCode();
|
||||||
|
return encodeTerminalMouseReport(mode, button, position, release: release);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,42 +1,294 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/gestures.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:xterm/xterm.dart';
|
import 'package:xterm/xterm.dart';
|
||||||
|
|
||||||
/// xterm 4.0.0 encodes wheel buttons as 68..71; the extra bit reads as a Shift
|
import 'platform_model.dart';
|
||||||
/// modifier, so strict full-screen apps ignore the report and never scroll.
|
import 'rustdesk_terminal.dart';
|
||||||
/// Upstream fix: TerminalStudio/xterm.dart#238.
|
import 'terminal_copy_shortcut.dart';
|
||||||
class WheelButtonFixMouseHandler implements TerminalMouseHandler {
|
import 'terminal_mouse_drag_reporter.dart';
|
||||||
const WheelButtonFixMouseHandler();
|
|
||||||
|
part 'terminal_mouse_handler_input.dart';
|
||||||
|
part 'terminal_web_clipboard_gesture.dart';
|
||||||
|
|
||||||
|
class TerminalMouseInteraction extends StatefulWidget {
|
||||||
|
const TerminalMouseInteraction(
|
||||||
|
this.terminal, {
|
||||||
|
super.key,
|
||||||
|
required this.controller,
|
||||||
|
this.focusNode,
|
||||||
|
this.autofocus = false,
|
||||||
|
this.textStyle = const TerminalStyle(),
|
||||||
|
this.deleteDetection = false,
|
||||||
|
this.reportTouchInput = false,
|
||||||
|
this.shortcuts,
|
||||||
|
this.onKeyEvent,
|
||||||
|
this.backgroundOpacity = 1,
|
||||||
|
this.padding,
|
||||||
|
this.onSecondaryTapDown,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Terminal terminal;
|
||||||
|
final TerminalController controller;
|
||||||
|
final FocusNode? focusNode;
|
||||||
|
final bool autofocus;
|
||||||
|
final TerminalStyle textStyle;
|
||||||
|
final bool deleteDetection;
|
||||||
|
final bool reportTouchInput;
|
||||||
|
final Map<ShortcutActivator, Intent>? shortcuts;
|
||||||
|
final FocusOnKeyEventCallback? onKeyEvent;
|
||||||
|
final double backgroundOpacity;
|
||||||
|
final EdgeInsets? padding;
|
||||||
|
final void Function(TapDownDetails, CellOffset)? onSecondaryTapDown;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String? call(TerminalMouseEvent event) {
|
State<TerminalMouseInteraction> createState() =>
|
||||||
if (!event.button.isWheel) {
|
_TerminalMouseInteractionState();
|
||||||
return defaultMouseHandler(event);
|
}
|
||||||
}
|
|
||||||
// Same gate as UpDownMouseHandler: only the scroll modes report a wheel,
|
class _TerminalMouseInteractionState extends State<TerminalMouseInteraction> {
|
||||||
// and a wheel release is never reported, so the report is always a press.
|
static const _selectionScrollInterval = Duration(milliseconds: 50);
|
||||||
if (!event.state.mouseMode.reportScroll ||
|
static const _noScroll = 0;
|
||||||
event.buttonState == TerminalMouseButtonState.up) {
|
static const _scrollUp = -1;
|
||||||
return null;
|
static const _scrollDown = 1;
|
||||||
}
|
|
||||||
return _reportWheel(event);
|
final _terminalViewKey = GlobalKey<TerminalViewState>();
|
||||||
|
final _scrollController = ScrollController();
|
||||||
|
final _mouseDrag = TerminalMouseDragReporter();
|
||||||
|
late final WheelButtonFixMouseHandler _mouseHandler;
|
||||||
|
TerminalMouseHandler? _previousMouseHandler;
|
||||||
|
Offset? _pointerPosition;
|
||||||
|
Offset? _selectionPointer;
|
||||||
|
CellAnchor? _selectionBase;
|
||||||
|
Buffer? _selectionBuffer;
|
||||||
|
int? _selectionPointerId;
|
||||||
|
Timer? _selectionScrollTimer;
|
||||||
|
Timer? _pendingTouchMouseTimer;
|
||||||
|
PointerDownEvent? _pendingTouchMouseDown;
|
||||||
|
var _selectionHasScrolled = false;
|
||||||
|
var _scrollDirection = _noScroll;
|
||||||
|
// xterm can finish its tap callbacks after the raw drag was reported.
|
||||||
|
var _suppressXtermLeftButton = false;
|
||||||
|
var _terminalClipboardGesturePrepared = false;
|
||||||
|
TerminalViewState? get _terminalView => _terminalViewKey.currentState;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_mouseHandler = WheelButtonFixMouseHandler(
|
||||||
|
positionProvider: _cellAtPointer,
|
||||||
|
suppressLeftButton: kIsWeb ? _consumeXtermLeftButtonSuppression : null,
|
||||||
|
);
|
||||||
|
_installMouseHandler(widget.terminal);
|
||||||
}
|
}
|
||||||
|
|
||||||
String _reportWheel(TerminalMouseEvent event) {
|
@override
|
||||||
// Wheel buttons 4..7 go on the wire as 64..67, but `id` is 64 + 4..7.
|
void didUpdateWidget(TerminalMouseInteraction oldWidget) {
|
||||||
final button = event.button.id - 4;
|
super.didUpdateWidget(oldWidget);
|
||||||
final x = event.position.x + 1;
|
final terminalChanged = !identical(oldWidget.terminal, widget.terminal);
|
||||||
final y = event.position.y + 1;
|
final controllerChanged =
|
||||||
switch (event.state.mouseReportMode) {
|
!identical(oldWidget.controller, widget.controller);
|
||||||
case MouseReportMode.normal:
|
final touchInputChanged =
|
||||||
case MouseReportMode.utf:
|
oldWidget.reportTouchInput != widget.reportTouchInput;
|
||||||
final limit =
|
if (!terminalChanged && !controllerChanged && !touchInputChanged) return;
|
||||||
event.state.mouseReportMode == MouseReportMode.normal ? 223 : 2015;
|
_cancelPendingTouchMouseDrag();
|
||||||
final col = x > limit ? '\x00' : String.fromCharCode(32 + x);
|
if (!terminalChanged && !controllerChanged) return;
|
||||||
final row = y > limit ? '\x00' : String.fromCharCode(32 + y);
|
if (controllerChanged && !terminalChanged) {
|
||||||
return '\x1b[M${String.fromCharCode(32 + button)}$col$row';
|
_mouseDrag.updateController(widget.controller);
|
||||||
case MouseReportMode.sgr:
|
} else {
|
||||||
return '\x1b[<$button;$x;${y}M';
|
_discardPendingTerminalClipboardWrites();
|
||||||
case MouseReportMode.urxvt:
|
_mouseDrag.cancel();
|
||||||
return '\x1b[${32 + button};$x;${y}M';
|
}
|
||||||
|
_clearSelectionDrag();
|
||||||
|
if (!terminalChanged) return;
|
||||||
|
_restoreMouseHandler(oldWidget.terminal);
|
||||||
|
_installMouseHandler(widget.terminal);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _installMouseHandler(Terminal terminal) {
|
||||||
|
_previousMouseHandler = terminal.mouseHandler;
|
||||||
|
terminal.mouseHandler = _mouseHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _restoreMouseHandler(Terminal terminal) {
|
||||||
|
if (identical(terminal.mouseHandler, _mouseHandler)) {
|
||||||
|
terminal.mouseHandler = _previousMouseHandler;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _handlePointerMove(PointerMoveEvent event) {
|
||||||
|
_updatePointerPosition(event);
|
||||||
|
if (_handlePendingTouchMove(event)) return;
|
||||||
|
if (_mouseDrag.handleMove(
|
||||||
|
event,
|
||||||
|
widget.terminal,
|
||||||
|
_terminalView,
|
||||||
|
beforeRelease: _finishTerminalClipboardWrite,
|
||||||
|
onCancel: _cancelTerminalClipboardWrite,
|
||||||
|
)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (event.pointer != _selectionPointerId) return;
|
||||||
|
if (event.kind != PointerDeviceKind.mouse ||
|
||||||
|
(event.buttons & kPrimaryMouseButton) != kPrimaryMouseButton) {
|
||||||
|
_clearSelectionDrag();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final terminalView = _terminalView;
|
||||||
|
if (terminalView == null || _selectionBase == null) return;
|
||||||
|
final renderTerminal = terminalView.renderTerminal;
|
||||||
|
final localPosition = renderTerminal.globalToLocal(event.position);
|
||||||
|
_selectionPointer = localPosition;
|
||||||
|
_setScrollDirection(
|
||||||
|
_directionFor(localPosition, renderTerminal.paintBounds),
|
||||||
|
);
|
||||||
|
if (_selectionHasScrolled) {
|
||||||
|
scheduleMicrotask(() => _scrollSelection(scroll: false));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int _directionFor(Offset position, Rect bounds) {
|
||||||
|
if (position.dy < bounds.top) return _scrollUp;
|
||||||
|
if (position.dy >= bounds.bottom) return _scrollDown;
|
||||||
|
return _noScroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _setScrollDirection(int direction) {
|
||||||
|
if (_scrollDirection == direction) return;
|
||||||
|
_stopAutoScroll();
|
||||||
|
_scrollDirection = direction;
|
||||||
|
if (direction == _noScroll) return;
|
||||||
|
_scrollSelection();
|
||||||
|
if (_scrollDirection != _noScroll) {
|
||||||
|
_selectionScrollTimer = Timer.periodic(
|
||||||
|
_selectionScrollInterval,
|
||||||
|
(_) => _scrollSelection(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _scrollSelection({bool scroll = true}) {
|
||||||
|
final terminalView = _terminalView;
|
||||||
|
final selectionBase = _selectionBase;
|
||||||
|
final selectionBuffer = _selectionBuffer;
|
||||||
|
final selectionPointer = _selectionPointer;
|
||||||
|
if (terminalView == null ||
|
||||||
|
selectionBase == null ||
|
||||||
|
selectionBuffer == null ||
|
||||||
|
selectionPointer == null ||
|
||||||
|
!_scrollController.hasClients) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!identical(selectionBuffer, widget.terminal.buffer) ||
|
||||||
|
!selectionBase.attached) {
|
||||||
|
_clearSelectionDrag();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final renderTerminal = terminalView.renderTerminal;
|
||||||
|
if (scroll) {
|
||||||
|
final position = _scrollController.position;
|
||||||
|
final target =
|
||||||
|
(position.pixels + renderTerminal.lineHeight * _scrollDirection)
|
||||||
|
.clamp(position.minScrollExtent, position.maxScrollExtent)
|
||||||
|
.toDouble();
|
||||||
|
if (target == position.pixels) {
|
||||||
|
_stopAutoScroll();
|
||||||
|
} else {
|
||||||
|
position.jumpTo(target);
|
||||||
|
_selectionHasScrolled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
renderTerminal.selectCharacters(
|
||||||
|
renderTerminal.getOffset(selectionBase.offset),
|
||||||
|
selectionPointer,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handlePointerEnd(PointerEvent event) {
|
||||||
|
_updatePointerPosition(event);
|
||||||
|
final pendingTouch = _pendingTouchMouseDown;
|
||||||
|
if (pendingTouch != null && pendingTouch.pointer == event.pointer) {
|
||||||
|
final movedBeyondSlop =
|
||||||
|
(event.position - pendingTouch.position).distance > kTouchSlop;
|
||||||
|
if (event is PointerUpEvent && !movedBeyondSlop) {
|
||||||
|
_activatePendingTouchMouseDrag(cancelOnFailure: false);
|
||||||
|
} else {
|
||||||
|
_takePendingTouchMouseDrag(pointer: event.pointer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final handledByMouseDrag = _mouseDrag.handleEnd(
|
||||||
|
event,
|
||||||
|
widget.terminal,
|
||||||
|
_terminalView,
|
||||||
|
beforeRelease: event is PointerUpEvent
|
||||||
|
? _finishTerminalClipboardWrite
|
||||||
|
: (_) => _cancelTerminalClipboardWrite(),
|
||||||
|
onCancel: _cancelTerminalClipboardWrite,
|
||||||
|
);
|
||||||
|
if (!handledByMouseDrag && event.pointer != _selectionPointerId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (_selectionHasScrolled) _scrollSelection(scroll: false);
|
||||||
|
_clearSelectionDrag();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _clearSelectionDrag() {
|
||||||
|
_selectionPointerId = null;
|
||||||
|
_selectionBase?.dispose();
|
||||||
|
_selectionBase = null;
|
||||||
|
_selectionBuffer = null;
|
||||||
|
_selectionPointer = null;
|
||||||
|
_selectionHasScrolled = false;
|
||||||
|
_stopAutoScroll();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _stopAutoScroll() {
|
||||||
|
_selectionScrollTimer?.cancel();
|
||||||
|
_selectionScrollTimer = null;
|
||||||
|
_scrollDirection = _noScroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_discardPendingTerminalClipboardWrites();
|
||||||
|
_cancelPendingTouchMouseDrag();
|
||||||
|
_mouseDrag.cancel();
|
||||||
|
_clearSelectionDrag();
|
||||||
|
_restoreMouseHandler(widget.terminal);
|
||||||
|
_scrollController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Listener(
|
||||||
|
onPointerDown: _handlePointerDown,
|
||||||
|
onPointerMove: _handlePointerMove,
|
||||||
|
onPointerUp: _handlePointerEnd,
|
||||||
|
onPointerHover: _updatePointerPosition,
|
||||||
|
onPointerCancel: _handlePointerEnd,
|
||||||
|
onPointerSignal: _updatePointerPosition,
|
||||||
|
onPointerPanZoomStart: _updatePointerPosition,
|
||||||
|
onPointerPanZoomUpdate: _updatePointerPosition,
|
||||||
|
onPointerPanZoomEnd: _updatePointerPosition,
|
||||||
|
child: TerminalView(
|
||||||
|
widget.terminal,
|
||||||
|
key: _terminalViewKey,
|
||||||
|
controller: widget.controller,
|
||||||
|
scrollController: _scrollController,
|
||||||
|
focusNode: widget.focusNode,
|
||||||
|
autofocus: widget.autofocus,
|
||||||
|
textStyle: widget.textStyle,
|
||||||
|
deleteDetection: widget.deleteDetection,
|
||||||
|
backgroundOpacity: widget.backgroundOpacity,
|
||||||
|
padding: widget.padding,
|
||||||
|
shortcuts: widget.shortcuts ?? platformTerminalShortcuts(),
|
||||||
|
onKeyEvent: widget.onKeyEvent ??
|
||||||
|
terminalCopyHandler(widget.terminal, widget.controller),
|
||||||
|
onSecondaryTapDown: widget.onSecondaryTapDown,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
162
flutter/lib/models/terminal_mouse_handler_input.dart
Normal file
162
flutter/lib/models/terminal_mouse_handler_input.dart
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
part of 'terminal_mouse_handler.dart';
|
||||||
|
|
||||||
|
/// xterm 4.0.0 encodes wheel buttons as 68..71; the extra bit reads as a Shift
|
||||||
|
/// modifier, so strict full-screen apps ignore the report and never scroll.
|
||||||
|
/// Upstream fix: TerminalStudio/xterm.dart#238.
|
||||||
|
class WheelButtonFixMouseHandler implements TerminalMouseHandler {
|
||||||
|
const WheelButtonFixMouseHandler({
|
||||||
|
this.positionProvider,
|
||||||
|
this.suppressLeftButton,
|
||||||
|
});
|
||||||
|
|
||||||
|
final CellOffset? Function()? positionProvider;
|
||||||
|
final bool Function(TerminalMouseButtonState)? suppressLeftButton;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String? call(TerminalMouseEvent event) {
|
||||||
|
if (!event.button.isWheel) {
|
||||||
|
if (event.button == TerminalMouseButton.left &&
|
||||||
|
suppressLeftButton?.call(event.buttonState) == true) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return defaultMouseHandler(event);
|
||||||
|
}
|
||||||
|
// Same gate as UpDownMouseHandler: only the scroll modes report a wheel,
|
||||||
|
// and a wheel release is never reported, so the report is always a press.
|
||||||
|
if (!event.state.mouseMode.reportScroll ||
|
||||||
|
event.buttonState == TerminalMouseButtonState.up) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return _reportWheel(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _reportWheel(TerminalMouseEvent event) {
|
||||||
|
// Wheel buttons 4..7 go on the wire as 64..67, but `id` is 64 + 4..7.
|
||||||
|
final button = event.button.id - 4;
|
||||||
|
final position = positionProvider?.call() ?? event.position;
|
||||||
|
return encodeTerminalMouseReport(
|
||||||
|
event.state.mouseReportMode,
|
||||||
|
button,
|
||||||
|
position,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension _TerminalMouseInput on _TerminalMouseInteractionState {
|
||||||
|
CellOffset? _cellAtPointer() {
|
||||||
|
final terminalView = _terminalView;
|
||||||
|
final pointerPosition = _pointerPosition;
|
||||||
|
if (terminalView == null || pointerPosition == null) return null;
|
||||||
|
final renderTerminal = terminalView.renderTerminal;
|
||||||
|
return renderTerminal.getCellOffset(
|
||||||
|
renderTerminal.globalToLocal(pointerPosition),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _updatePointerPosition(PointerEvent event) =>
|
||||||
|
_pointerPosition = event.position;
|
||||||
|
|
||||||
|
void _handlePointerDown(PointerDownEvent event) {
|
||||||
|
_updatePointerPosition(event);
|
||||||
|
_suppressXtermLeftButton = false;
|
||||||
|
if (_startPendingTouchMouseDrag(event)) return;
|
||||||
|
if (_mouseDrag.handleDown(event, widget.terminal, _terminalView)) {
|
||||||
|
_prepareTerminalClipboardWrite();
|
||||||
|
if (kIsWeb) _suppressXtermLeftButton = true;
|
||||||
|
_clearSelectionDrag();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (event.kind != PointerDeviceKind.mouse ||
|
||||||
|
(event.buttons & kPrimaryMouseButton) != kPrimaryMouseButton) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_clearSelectionDrag();
|
||||||
|
final terminalView = _terminalView;
|
||||||
|
if (terminalView == null) return;
|
||||||
|
final renderTerminal = terminalView.renderTerminal;
|
||||||
|
final localPosition = renderTerminal.globalToLocal(event.position);
|
||||||
|
final selectionBuffer = widget.terminal.buffer;
|
||||||
|
_selectionPointerId = event.pointer;
|
||||||
|
_selectionBase = selectionBuffer.createAnchorFromOffset(
|
||||||
|
renderTerminal.getCellOffset(localPosition),
|
||||||
|
);
|
||||||
|
_selectionBuffer = selectionBuffer;
|
||||||
|
_selectionPointer = localPosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _startPendingTouchMouseDrag(PointerDownEvent event) {
|
||||||
|
if (!widget.reportTouchInput ||
|
||||||
|
event.kind != PointerDeviceKind.touch ||
|
||||||
|
!_mouseDrag.handleDown(
|
||||||
|
event,
|
||||||
|
widget.terminal,
|
||||||
|
_terminalView,
|
||||||
|
reportTouchInput: true,
|
||||||
|
deferReport: true,
|
||||||
|
)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
_pendingTouchMouseDown = event;
|
||||||
|
_pendingTouchMouseTimer = Timer(
|
||||||
|
kLongPressTimeout,
|
||||||
|
_activatePendingTouchMouseDrag,
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _activatePendingTouchMouseDrag({
|
||||||
|
bool cancelOnFailure = true,
|
||||||
|
}) {
|
||||||
|
if (_takePendingTouchMouseDrag() == null) return false;
|
||||||
|
if (_mouseDrag.activateDeferredDown(widget.terminal)) {
|
||||||
|
_prepareTerminalClipboardWrite();
|
||||||
|
_clearSelectionDrag();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (cancelOnFailure) _mouseDrag.cancel();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
PointerDownEvent? _takePendingTouchMouseDrag({int? pointer}) {
|
||||||
|
final pending = _pendingTouchMouseDown;
|
||||||
|
if (pending == null || pointer != null && pointer != pending.pointer) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
_pendingTouchMouseTimer?.cancel();
|
||||||
|
_pendingTouchMouseTimer = null;
|
||||||
|
_pendingTouchMouseDown = null;
|
||||||
|
return pending;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _cancelPendingTouchMouseDrag({
|
||||||
|
int? pointer,
|
||||||
|
bool deferCancel = false,
|
||||||
|
}) {
|
||||||
|
if (_takePendingTouchMouseDrag(pointer: pointer) == null) return;
|
||||||
|
if (deferCancel) {
|
||||||
|
scheduleMicrotask(_mouseDrag.cancel);
|
||||||
|
} else {
|
||||||
|
_mouseDrag.cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _handlePendingTouchMove(PointerMoveEvent event) {
|
||||||
|
final pending = _pendingTouchMouseDown;
|
||||||
|
if (pending == null || pending.pointer != event.pointer) return false;
|
||||||
|
if ((event.position - pending.position).distance > kTouchSlop) {
|
||||||
|
_cancelPendingTouchMouseDrag(
|
||||||
|
pointer: event.pointer,
|
||||||
|
deferCancel: true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _consumeXtermLeftButtonSuppression(TerminalMouseButtonState state) {
|
||||||
|
final suppress = _suppressXtermLeftButton;
|
||||||
|
if (state == TerminalMouseButtonState.up) {
|
||||||
|
_suppressXtermLeftButton = false;
|
||||||
|
}
|
||||||
|
return suppress;
|
||||||
|
}
|
||||||
|
}
|
||||||
56
flutter/lib/models/terminal_web_clipboard_gesture.dart
Normal file
56
flutter/lib/models/terminal_web_clipboard_gesture.dart
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
part of 'terminal_mouse_handler.dart';
|
||||||
|
|
||||||
|
const _prepareTerminalClipboardCommand = 'prepare_terminal_clipboard';
|
||||||
|
const _finishTerminalClipboardCommand = 'finish_terminal_clipboard';
|
||||||
|
const _cancelTerminalClipboardCommand = 'cancel_terminal_clipboard';
|
||||||
|
|
||||||
|
extension _TerminalWebClipboardGesture on _TerminalMouseInteractionState {
|
||||||
|
void _prepareTerminalClipboardWrite() {
|
||||||
|
if (!kIsWeb) return;
|
||||||
|
_cancelTerminalClipboardWrite();
|
||||||
|
final terminal = widget.terminal;
|
||||||
|
if (terminal is! RustDeskTerminal || !terminal.isClipboardWriteAllowed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
ffiSetByName(_prepareTerminalClipboardCommand);
|
||||||
|
_terminalClipboardGesturePrepared = true;
|
||||||
|
} catch (error) {
|
||||||
|
debugPrint('[Terminal] Failed to prepare Web clipboard write: $error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _finishTerminalClipboardWrite(bool responseExpected) {
|
||||||
|
if (!_terminalClipboardGesturePrepared) return;
|
||||||
|
_terminalClipboardGesturePrepared = false;
|
||||||
|
if (!kIsWeb) return;
|
||||||
|
try {
|
||||||
|
ffiSetByName(
|
||||||
|
_finishTerminalClipboardCommand,
|
||||||
|
responseExpected ? 'true' : 'false',
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
debugPrint('[Terminal] Failed to finish Web clipboard write: $error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _cancelTerminalClipboardWrite() {
|
||||||
|
if (!_terminalClipboardGesturePrepared) return;
|
||||||
|
_terminalClipboardGesturePrepared = false;
|
||||||
|
_sendTerminalClipboardCancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _discardPendingTerminalClipboardWrites() {
|
||||||
|
_cancelTerminalClipboardWrite();
|
||||||
|
_sendTerminalClipboardCancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _sendTerminalClipboardCancel() {
|
||||||
|
if (!kIsWeb) return;
|
||||||
|
try {
|
||||||
|
ffiSetByName(_cancelTerminalClipboardCommand);
|
||||||
|
} catch (error) {
|
||||||
|
debugPrint('[Terminal] Failed to cancel Web clipboard write: $error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -136,12 +136,6 @@ class PlatformFFI {
|
|||||||
void registerGpuTexture(SessionID sessionId, int display, int ptr) =>
|
void registerGpuTexture(SessionID sessionId, int display, int ptr) =>
|
||||||
_ffiBind.sessionRegisterGpuTexture(
|
_ffiBind.sessionRegisterGpuTexture(
|
||||||
sessionId: sessionId, display: display, ptr: ptr);
|
sessionId: sessionId, display: display, ptr: ptr);
|
||||||
void unregisterPixelbufferTexture(SessionID sessionId, int display, int ptr) =>
|
|
||||||
_ffiBind.sessionUnregisterPixelbufferTexture(
|
|
||||||
sessionId: sessionId, display: display, ptr: ptr);
|
|
||||||
void unregisterGpuTexture(SessionID sessionId, int display, int ptr) =>
|
|
||||||
_ffiBind.sessionUnregisterGpuTexture(
|
|
||||||
sessionId: sessionId, display: display, ptr: ptr);
|
|
||||||
|
|
||||||
Future<void> init(String appType) async {
|
Future<void> init(String appType) async {
|
||||||
Completer completer = Completer();
|
Completer completer = Completer();
|
||||||
@@ -257,6 +251,11 @@ class PlatformFFI {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<T?> invokeMethodWithResult<T>(String method,
|
||||||
|
[dynamic arguments]) async {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
// just for compilation
|
// just for compilation
|
||||||
void syncAndroidServiceAppDirConfigPath() {}
|
void syncAndroidServiceAppDirConfigPath() {}
|
||||||
|
|
||||||
|
|||||||
@@ -10,8 +10,6 @@ final isWebDesktop_ = false;
|
|||||||
|
|
||||||
final isDesktop_ = Platform.isWindows || Platform.isMacOS || Platform.isLinux;
|
final isDesktop_ = Platform.isWindows || Platform.isMacOS || Platform.isLinux;
|
||||||
|
|
||||||
String get screenInfo_ => '';
|
|
||||||
|
|
||||||
final isWebOnWindows_ = false;
|
final isWebOnWindows_ = false;
|
||||||
final isWebOnLinux_ = false;
|
final isWebOnLinux_ = false;
|
||||||
final isWebOnMacOS_ = false;
|
final isWebOnMacOS_ = false;
|
||||||
|
|||||||
@@ -1,42 +0,0 @@
|
|||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
typedef PluginId = String;
|
|
||||||
|
|
||||||
// ui location
|
|
||||||
const String kLocationHostMainPlugin = 'host|main|settings|plugin';
|
|
||||||
const String kLocationClientRemoteToolbarDisplay =
|
|
||||||
'client|remote|toolbar|display';
|
|
||||||
|
|
||||||
class MsgFromUi {
|
|
||||||
String id;
|
|
||||||
String name;
|
|
||||||
String location;
|
|
||||||
String key;
|
|
||||||
String value;
|
|
||||||
String action;
|
|
||||||
|
|
||||||
MsgFromUi({
|
|
||||||
required this.id,
|
|
||||||
required this.name,
|
|
||||||
required this.location,
|
|
||||||
required this.key,
|
|
||||||
required this.value,
|
|
||||||
required this.action,
|
|
||||||
});
|
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
|
||||||
return <String, dynamic>{
|
|
||||||
'id': id,
|
|
||||||
'name': name,
|
|
||||||
'location': location,
|
|
||||||
'key': key,
|
|
||||||
'value': value,
|
|
||||||
'action': action,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() {
|
|
||||||
return jsonEncode(toJson());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import 'dart:convert';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
void handlePluginEvent(
|
|
||||||
Map<String, dynamic> evt,
|
|
||||||
Function(Map<String, dynamic> e) handleMsgBox,
|
|
||||||
) {
|
|
||||||
Map<String, dynamic>? content;
|
|
||||||
try {
|
|
||||||
content = json.decode(evt['content']);
|
|
||||||
} catch (e) {
|
|
||||||
debugPrint(
|
|
||||||
'Json decode plugin event content failed: $e, ${evt['content']}');
|
|
||||||
}
|
|
||||||
if (content?['t'] == 'MsgBox') {
|
|
||||||
handleMsgBox(content?['c']);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
import 'dart:convert';
|
|
||||||
import 'dart:ffi';
|
|
||||||
|
|
||||||
import 'package:ffi/ffi.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_hbb/plugin/ui_manager.dart';
|
|
||||||
import 'package:flutter_hbb/plugin/utils/dialogs.dart';
|
|
||||||
|
|
||||||
abstract class NativeHandler {
|
|
||||||
bool onEvent(Map<String, dynamic> evt);
|
|
||||||
}
|
|
||||||
|
|
||||||
typedef OnSelectPeersCallback = Bool Function(Int returnCode,
|
|
||||||
Pointer<Void> data, Uint64 dataLength, Pointer<Void> userData);
|
|
||||||
typedef OnSelectPeersCallbackDart = bool Function(
|
|
||||||
int returnCode, Pointer<Void> data, int dataLength, Pointer<Void> userData);
|
|
||||||
|
|
||||||
class NativeUiHandler extends NativeHandler {
|
|
||||||
NativeUiHandler._();
|
|
||||||
|
|
||||||
static NativeUiHandler instance = NativeUiHandler._();
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool onEvent(Map<String, dynamic> evt) {
|
|
||||||
final name = evt['name'];
|
|
||||||
final action = evt['action'];
|
|
||||||
if (name != "native_ui") {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
switch (action) {
|
|
||||||
case "select_peers":
|
|
||||||
int cb = evt['cb'];
|
|
||||||
int userData = evt['user_data'] ?? 0;
|
|
||||||
final cbFuncNative = Pointer.fromAddress(cb)
|
|
||||||
.cast<NativeFunction<OnSelectPeersCallback>>();
|
|
||||||
final cbFuncDart = cbFuncNative.asFunction<OnSelectPeersCallbackDart>();
|
|
||||||
onSelectPeers(cbFuncDart, userData);
|
|
||||||
break;
|
|
||||||
case "register_ui_entry":
|
|
||||||
int cb = evt['on_tap_cb'];
|
|
||||||
int userData = evt['user_data'] ?? 0;
|
|
||||||
String title = evt['title'] ?? "";
|
|
||||||
final cbFuncNative = Pointer.fromAddress(cb)
|
|
||||||
.cast<NativeFunction<OnSelectPeersCallback>>();
|
|
||||||
final cbFuncDart = cbFuncNative.asFunction<OnSelectPeersCallbackDart>();
|
|
||||||
onRegisterUiEntry(title, cbFuncDart, userData);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void onSelectPeers(OnSelectPeersCallbackDart cb, int userData) async {
|
|
||||||
showPeerSelectionDialog(onPeersCallback: (peers) {
|
|
||||||
String json = jsonEncode(<String, dynamic> {
|
|
||||||
"peers": peers
|
|
||||||
});
|
|
||||||
final native = json.toNativeUtf8();
|
|
||||||
cb(0, native.cast(), native.length, Pointer.fromAddress(userData));
|
|
||||||
malloc.free(native);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void onRegisterUiEntry(String title, OnSelectPeersCallbackDart cbFuncDart, int userData) {
|
|
||||||
Widget widget = InkWell(
|
|
||||||
child: Container(
|
|
||||||
height: 25.0,
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Expanded(child: Text(title)),
|
|
||||||
Icon(Icons.chevron_right_rounded, size: 12.0,)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
PluginUiManager.instance.registerEntry(title, widget);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,319 +0,0 @@
|
|||||||
// The plugin manager is a singleton class that manages the plugins.
|
|
||||||
// 1. It merge metadata and the desc of plugins.
|
|
||||||
|
|
||||||
import 'dart:convert';
|
|
||||||
import 'dart:collection';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
const String kValueTrue = '1';
|
|
||||||
const String kValueFalse = '0';
|
|
||||||
|
|
||||||
class ConfigItem {
|
|
||||||
String key;
|
|
||||||
String description;
|
|
||||||
String defaultValue;
|
|
||||||
|
|
||||||
ConfigItem(this.key, this.defaultValue, this.description);
|
|
||||||
ConfigItem.fromJson(Map<String, dynamic> json)
|
|
||||||
: key = json['key'] ?? '',
|
|
||||||
description = json['description'] ?? '',
|
|
||||||
defaultValue = json['default'] ?? '';
|
|
||||||
|
|
||||||
static String get trueValue => kValueTrue;
|
|
||||||
static String get falseValue => kValueFalse;
|
|
||||||
static bool isTrue(String value) => value == kValueTrue;
|
|
||||||
static bool isFalse(String value) => value == kValueFalse;
|
|
||||||
}
|
|
||||||
|
|
||||||
class UiType {
|
|
||||||
String key;
|
|
||||||
String text;
|
|
||||||
String tooltip;
|
|
||||||
String action;
|
|
||||||
|
|
||||||
UiType(this.key, this.text, this.tooltip, this.action);
|
|
||||||
|
|
||||||
UiType.fromJson(Map<String, dynamic> json)
|
|
||||||
: key = json['key'] ?? '',
|
|
||||||
text = json['text'] ?? '',
|
|
||||||
tooltip = json['tooltip'] ?? '',
|
|
||||||
action = json['action'] ?? '';
|
|
||||||
|
|
||||||
static UiType? create(Map<String, dynamic> json) {
|
|
||||||
if (json['t'] == 'Button') {
|
|
||||||
return UiButton.fromJson(json['c']);
|
|
||||||
} else if (json['t'] == 'Checkbox') {
|
|
||||||
return UiCheckbox.fromJson(json['c']);
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class UiButton extends UiType {
|
|
||||||
String icon;
|
|
||||||
|
|
||||||
UiButton(
|
|
||||||
{required String key,
|
|
||||||
required String text,
|
|
||||||
required this.icon,
|
|
||||||
required String tooltip,
|
|
||||||
required String action})
|
|
||||||
: super(key, text, tooltip, action);
|
|
||||||
|
|
||||||
UiButton.fromJson(Map<String, dynamic> json)
|
|
||||||
: icon = json['icon'] ?? '',
|
|
||||||
super.fromJson(json);
|
|
||||||
}
|
|
||||||
|
|
||||||
class UiCheckbox extends UiType {
|
|
||||||
UiCheckbox(
|
|
||||||
{required String key,
|
|
||||||
required String text,
|
|
||||||
required String tooltip,
|
|
||||||
required String action})
|
|
||||||
: super(key, text, tooltip, action);
|
|
||||||
|
|
||||||
UiCheckbox.fromJson(Map<String, dynamic> json) : super.fromJson(json);
|
|
||||||
}
|
|
||||||
|
|
||||||
class Location {
|
|
||||||
// location key:
|
|
||||||
// host|main|settings|plugin
|
|
||||||
// client|remote|toolbar|display
|
|
||||||
HashMap<String, UiType> ui;
|
|
||||||
|
|
||||||
Location(this.ui);
|
|
||||||
Location.fromJson(Map<String, dynamic> json) : ui = HashMap() {
|
|
||||||
(json['ui'] as Map<String, dynamic>).forEach((key, value) {
|
|
||||||
var ui = UiType.create(value);
|
|
||||||
if (ui != null) {
|
|
||||||
this.ui[ui.key] = ui;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class PublishInfo {
|
|
||||||
PublishInfo({
|
|
||||||
required this.lastReleased,
|
|
||||||
required this.published,
|
|
||||||
});
|
|
||||||
|
|
||||||
final DateTime lastReleased;
|
|
||||||
final DateTime published;
|
|
||||||
}
|
|
||||||
|
|
||||||
class Meta {
|
|
||||||
Meta({
|
|
||||||
required this.id,
|
|
||||||
required this.name,
|
|
||||||
required this.version,
|
|
||||||
required this.description,
|
|
||||||
required this.author,
|
|
||||||
required this.home,
|
|
||||||
required this.license,
|
|
||||||
required this.publishInfo,
|
|
||||||
required this.source,
|
|
||||||
});
|
|
||||||
|
|
||||||
final String id;
|
|
||||||
final String name;
|
|
||||||
final String version;
|
|
||||||
final String description;
|
|
||||||
final String author;
|
|
||||||
final String home;
|
|
||||||
final String license;
|
|
||||||
final PublishInfo publishInfo;
|
|
||||||
final String source;
|
|
||||||
}
|
|
||||||
|
|
||||||
class SourceInfo {
|
|
||||||
String name; // 1. RustDesk github 2. Local
|
|
||||||
String url;
|
|
||||||
String description;
|
|
||||||
|
|
||||||
SourceInfo({
|
|
||||||
required this.name,
|
|
||||||
required this.url,
|
|
||||||
required this.description,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
class PluginInfo with ChangeNotifier {
|
|
||||||
SourceInfo sourceInfo;
|
|
||||||
Meta meta;
|
|
||||||
String installedVersion; // It is empty if not installed.
|
|
||||||
String failedMsg;
|
|
||||||
String invalidReason; // It is empty if valid.
|
|
||||||
|
|
||||||
PluginInfo({
|
|
||||||
required this.sourceInfo,
|
|
||||||
required this.meta,
|
|
||||||
required this.installedVersion,
|
|
||||||
required this.invalidReason,
|
|
||||||
this.failedMsg = '',
|
|
||||||
});
|
|
||||||
|
|
||||||
bool get installed => installedVersion.isNotEmpty;
|
|
||||||
bool get needUpdate => installed && installedVersion != meta.version;
|
|
||||||
|
|
||||||
void setInstall(String msg) {
|
|
||||||
if (msg == "finished") {
|
|
||||||
msg = '';
|
|
||||||
}
|
|
||||||
failedMsg = msg;
|
|
||||||
if (msg.isEmpty) {
|
|
||||||
installedVersion = meta.version;
|
|
||||||
}
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
void setUninstall(String msg) {
|
|
||||||
failedMsg = msg;
|
|
||||||
if (msg.isEmpty) {
|
|
||||||
installedVersion = '';
|
|
||||||
}
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class PluginManager with ChangeNotifier {
|
|
||||||
String failedReason = ''; // The reason of failed to load plugins.
|
|
||||||
final List<PluginInfo> _plugins = [];
|
|
||||||
|
|
||||||
PluginManager._();
|
|
||||||
static final PluginManager _instance = PluginManager._();
|
|
||||||
static PluginManager get instance => _instance;
|
|
||||||
|
|
||||||
List<PluginInfo> get plugins => _plugins;
|
|
||||||
|
|
||||||
PluginInfo? getPlugin(String id) {
|
|
||||||
for (var p in _plugins) {
|
|
||||||
if (p.meta.id == id) {
|
|
||||||
return p;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
void handleEvent(Map<String, dynamic> evt) {
|
|
||||||
if (evt['plugin_list'] != null) {
|
|
||||||
_handlePluginList(evt['plugin_list']);
|
|
||||||
} else if (evt['plugin_install'] != null && evt['id'] != null) {
|
|
||||||
_handlePluginInstall(evt['id'], evt['plugin_install']);
|
|
||||||
} else if (evt['plugin_uninstall'] != null && evt['id'] != null) {
|
|
||||||
_handlePluginUninstall(evt['id'], evt['plugin_uninstall']);
|
|
||||||
} else {
|
|
||||||
debugPrint('Failed to handle manager event: $evt');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _sortPlugins() {
|
|
||||||
plugins.sort((a, b) {
|
|
||||||
if (a.installed) {
|
|
||||||
return -1;
|
|
||||||
} else if (b.installed) {
|
|
||||||
return 1;
|
|
||||||
} else {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void _handlePluginList(String pluginList) {
|
|
||||||
_plugins.clear();
|
|
||||||
try {
|
|
||||||
for (var p in json.decode(pluginList) as List<dynamic>) {
|
|
||||||
final plugin = _getPluginFromEvent(p);
|
|
||||||
if (plugin == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
_plugins.add(plugin);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
debugPrint('Failed to decode $e, plugin list \'$pluginList\'');
|
|
||||||
}
|
|
||||||
_sortPlugins();
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _handlePluginInstall(String id, String msg) {
|
|
||||||
debugPrint('Plugin \'$id\' install msg $msg');
|
|
||||||
for (var i = 0; i < _plugins.length; i++) {
|
|
||||||
if (_plugins[i].meta.id == id) {
|
|
||||||
_plugins[i].setInstall(msg);
|
|
||||||
_sortPlugins();
|
|
||||||
notifyListeners();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _handlePluginUninstall(String id, String msg) {
|
|
||||||
debugPrint('Plugin \'$id\' uninstall msg $msg');
|
|
||||||
for (var i = 0; i < _plugins.length; i++) {
|
|
||||||
if (_plugins[i].meta.id == id) {
|
|
||||||
_plugins[i].setUninstall(msg);
|
|
||||||
_sortPlugins();
|
|
||||||
notifyListeners();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
PluginInfo? _getPluginFromEvent(Map<String, dynamic> evt) {
|
|
||||||
final s = evt['source'];
|
|
||||||
assert(s != null, 'Source is null');
|
|
||||||
if (s == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
final source = SourceInfo(
|
|
||||||
name: s['name'],
|
|
||||||
url: s['url'] ?? '',
|
|
||||||
description: s['description'] ?? '',
|
|
||||||
);
|
|
||||||
|
|
||||||
final m = evt['meta'];
|
|
||||||
assert(m != null, 'Meta is null');
|
|
||||||
if (m == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
late DateTime lastReleased;
|
|
||||||
late DateTime published;
|
|
||||||
try {
|
|
||||||
lastReleased = DateTime.parse(
|
|
||||||
m['publish_info']?['last_released'] ?? '1970-01-01T00+00:00');
|
|
||||||
} catch (e) {
|
|
||||||
lastReleased = DateTime.utc(1970);
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
published = DateTime.parse(
|
|
||||||
m['publish_info']?['published'] ?? '1970-01-01T00+00:00');
|
|
||||||
} catch (e) {
|
|
||||||
published = DateTime.utc(1970);
|
|
||||||
}
|
|
||||||
|
|
||||||
final meta = Meta(
|
|
||||||
id: m['id'],
|
|
||||||
name: m['name'],
|
|
||||||
version: m['version'],
|
|
||||||
description: m['description'] ?? '',
|
|
||||||
author: m['author'],
|
|
||||||
home: m['home'] ?? '',
|
|
||||||
license: m['license'] ?? '',
|
|
||||||
source: m['source'] ?? '',
|
|
||||||
publishInfo:
|
|
||||||
PublishInfo(lastReleased: lastReleased, published: published),
|
|
||||||
);
|
|
||||||
return PluginInfo(
|
|
||||||
sourceInfo: source,
|
|
||||||
meta: meta,
|
|
||||||
installedVersion: evt['installed_version'],
|
|
||||||
invalidReason: evt['invalid_reason'] ?? '',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
PluginManager get pluginManager => PluginManager.instance;
|
|
||||||
@@ -1,110 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import './common.dart';
|
|
||||||
import './manager.dart';
|
|
||||||
|
|
||||||
final Map<String, LocationModel> _locationModels = {};
|
|
||||||
final Map<String, OptionModel> _optionModels = {};
|
|
||||||
|
|
||||||
class OptionModel with ChangeNotifier {
|
|
||||||
String? v;
|
|
||||||
|
|
||||||
String? get value => v;
|
|
||||||
set value(String? v) {
|
|
||||||
this.v = v;
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
static String key(String location, PluginId id, String peer, String k) =>
|
|
||||||
'$location|$id|$peer|$k';
|
|
||||||
}
|
|
||||||
|
|
||||||
class PluginModel with ChangeNotifier {
|
|
||||||
final List<UiType> uiList = [];
|
|
||||||
final Map<String, String> opts = {};
|
|
||||||
|
|
||||||
void add(List<UiType> uiList) {
|
|
||||||
bool found = false;
|
|
||||||
for (var ui in uiList) {
|
|
||||||
for (int i = 0; i < this.uiList.length; i++) {
|
|
||||||
if (this.uiList[i].key == ui.key) {
|
|
||||||
this.uiList[i] = ui;
|
|
||||||
found = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!found) {
|
|
||||||
this.uiList.add(ui);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
String? getOpt(String key) => opts.remove(key);
|
|
||||||
|
|
||||||
bool get isEmpty => uiList.isEmpty;
|
|
||||||
}
|
|
||||||
|
|
||||||
class LocationModel with ChangeNotifier {
|
|
||||||
final Map<PluginId, PluginModel> pluginModels = {};
|
|
||||||
|
|
||||||
void add(PluginId id, List<UiType> uiList) {
|
|
||||||
if (pluginModels[id] != null) {
|
|
||||||
pluginModels[id]!.add(uiList);
|
|
||||||
} else {
|
|
||||||
var model = PluginModel();
|
|
||||||
model.add(uiList);
|
|
||||||
pluginModels[id] = model;
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void clear() {
|
|
||||||
pluginModels.clear();
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
void remove(PluginId id) {
|
|
||||||
pluginModels.remove(id);
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool get isEmpty => pluginModels.isEmpty;
|
|
||||||
}
|
|
||||||
|
|
||||||
void addLocationUi(String location, PluginId id, List<UiType> uiList) {
|
|
||||||
if (_locationModels[location] == null) {
|
|
||||||
_locationModels[location] = LocationModel();
|
|
||||||
}
|
|
||||||
_locationModels[location]?.add(id, uiList);
|
|
||||||
}
|
|
||||||
|
|
||||||
LocationModel? getLocationModel(String location) => _locationModels[location];
|
|
||||||
|
|
||||||
PluginModel? getPluginModel(String location, PluginId id) =>
|
|
||||||
_locationModels[location]?.pluginModels[id];
|
|
||||||
|
|
||||||
void clearPlugin(PluginId pluginId) {
|
|
||||||
for (var element in _locationModels.values) {
|
|
||||||
element.remove(pluginId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void clearLocations() {
|
|
||||||
for (var element in _locationModels.values) {
|
|
||||||
element.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
OptionModel getOptionModel(
|
|
||||||
String location, PluginId pluginId, String peer, String key) {
|
|
||||||
final k = OptionModel.key(location, pluginId, peer, key);
|
|
||||||
if (_optionModels[k] == null) {
|
|
||||||
_optionModels[k] = OptionModel();
|
|
||||||
}
|
|
||||||
return _optionModels[k]!;
|
|
||||||
}
|
|
||||||
|
|
||||||
void updateOption(
|
|
||||||
String location, PluginId id, String peer, String key, String value) {
|
|
||||||
final k = OptionModel.key(location, id, peer, key);
|
|
||||||
_optionModels[k]?.value = value;
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
class PluginUiManager {
|
|
||||||
PluginUiManager._();
|
|
||||||
|
|
||||||
static PluginUiManager instance = PluginUiManager._();
|
|
||||||
|
|
||||||
Map<String, Widget> entries = <String, Widget>{};
|
|
||||||
|
|
||||||
void registerEntry(String key, Widget widget) {
|
|
||||||
entries[key] = widget;
|
|
||||||
}
|
|
||||||
|
|
||||||
void unregisterEntry(String key) {
|
|
||||||
entries.remove(key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_hbb/common.dart';
|
|
||||||
|
|
||||||
void showPeerSelectionDialog(
|
|
||||||
{bool singleSelection = false,
|
|
||||||
required Function(List<String>) onPeersCallback}) async {
|
|
||||||
// load recent peers, we can directly use the peers in `gFFI.recentPeersModel`.
|
|
||||||
// The plugin is not used for now, so just left it empty here.
|
|
||||||
final peers = '';
|
|
||||||
if (peers.isEmpty) {
|
|
||||||
// debugPrint("load recent peers failed.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, dynamic> map = jsonDecode(peers);
|
|
||||||
List<dynamic> peersList = map['peers'] ?? [];
|
|
||||||
final selected = List<String>.empty(growable: true);
|
|
||||||
|
|
||||||
submit() async {
|
|
||||||
onPeersCallback.call(selected);
|
|
||||||
}
|
|
||||||
|
|
||||||
gFFI.dialogManager.show((setState, close, context) {
|
|
||||||
return CustomAlertDialog(
|
|
||||||
title:
|
|
||||||
Text(translate(singleSelection ? "Select peers" : "Select a peer")),
|
|
||||||
content: SizedBox(
|
|
||||||
height: 300.0,
|
|
||||||
child: ListView.builder(
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final Map<String, dynamic> peer = peersList[index];
|
|
||||||
final String platform = peer['platform'] ?? "";
|
|
||||||
final String id = peer['id'] ?? "";
|
|
||||||
final String alias = peer['alias'] ?? "";
|
|
||||||
return GestureDetector(
|
|
||||||
onTap: () {
|
|
||||||
setState(() {
|
|
||||||
if (selected.contains(id)) {
|
|
||||||
selected.remove(id);
|
|
||||||
} else {
|
|
||||||
selected.add(id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
child: Container(
|
|
||||||
key: ValueKey(index),
|
|
||||||
height: 50.0,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Theme.of(context).highlightColor,
|
|
||||||
borderRadius: BorderRadius.circular(12.0)),
|
|
||||||
padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 4.0),
|
|
||||||
margin: EdgeInsets.symmetric(vertical: 4.0),
|
|
||||||
child: Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
mainAxisSize: MainAxisSize.max,
|
|
||||||
children: [
|
|
||||||
// platform
|
|
||||||
SizedBox(
|
|
||||||
width: 8.0,
|
|
||||||
),
|
|
||||||
Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
getPlatformImage(platform, size: 34.0),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
SizedBox(
|
|
||||||
width: 8.0,
|
|
||||||
),
|
|
||||||
// id/alias
|
|
||||||
Expanded(child: Text(alias.isEmpty ? id : alias)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
itemCount: peersList.length,
|
|
||||||
itemExtent: 50.0,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
onSubmit: submit,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user