mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-09-13 16:01:00 +03:00
Compare commits
127 Commits
fix-textur
...
c4df2e6da2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c4df2e6da2 | ||
|
|
b473977aa8 | ||
|
|
81b55dff24 | ||
|
|
9eae353ee5 | ||
|
|
a02063ece8 | ||
|
|
e677fd510a | ||
|
|
7280238993 | ||
|
|
019dd20b30 | ||
|
|
98c0401134 | ||
|
|
3432b8bc30 | ||
|
|
fe1300584c | ||
|
|
6ffb506961 | ||
|
|
8f81561918 | ||
|
|
093790875b | ||
|
|
c99ccdcb0d | ||
|
|
e3ef5317c5 | ||
|
|
59b7987b1d | ||
|
|
00005f6af5 | ||
|
|
6926ad290f | ||
|
|
ef642ae329 | ||
|
|
fa48b3f88d | ||
|
|
d6a670e764 | ||
|
|
39a266cfd5 | ||
|
|
1e00d09f84 | ||
|
|
7a0705bdad | ||
|
|
bfd3a9d7d9 | ||
|
|
2786d12a28 | ||
|
|
cf9c94a677 | ||
|
|
db3830a781 | ||
|
|
2befaa5b6a | ||
|
|
b199024082 | ||
|
|
f578fa189f | ||
|
|
ed09f3cc76 | ||
|
|
2657143712 | ||
|
|
51bbaa3f17 | ||
|
|
09e27370a3 | ||
|
|
0412726a20 | ||
|
|
b319cef200 | ||
|
|
c81c85fb78 | ||
|
|
553d66035e | ||
|
|
4ed2a60e35 | ||
|
|
305b3e5e48 | ||
|
|
5a790443a8 | ||
|
|
47b2c05e1d | ||
|
|
120fc4f4c5 | ||
|
|
55cbd1a218 | ||
|
|
0e61153a94 | ||
|
|
65a819f9e2 | ||
|
|
3d5406aa1c | ||
|
|
c40cb2a93e | ||
|
|
c631cdcf22 | ||
|
|
2f8c8704bb | ||
|
|
b591eeaa76 | ||
|
|
21cdf4d0a1 | ||
|
|
5ce320d1cd | ||
|
|
32ee0c19e3 | ||
|
|
0a135e0fae | ||
|
|
d3d5690bbf | ||
|
|
7e80399689 | ||
|
|
777d6347e4 | ||
|
|
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=825a0a4862818f74406d8e1cc25be72259228235#825a0a4862818f74406d8e1cc25be72259228235"
|
||||||
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=825a0a4862818f74406d8e1cc25be72259228235#825a0a4862818f74406d8e1cc25be72259228235"
|
||||||
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]]
|
||||||
|
|||||||
22
Cargo.toml
22
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,22 @@ 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.
|
||||||
|
# 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 = "825a0a4862818f74406d8e1cc25be72259228235" }
|
||||||
|
webrtc-sctp = { git = "https://github.com/rustdesk-org/webrtc", rev = "825a0a4862818f74406d8e1cc25be72259228235" }
|
||||||
|
|
||||||
[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 +244,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;
|
||||||
|
|||||||
@@ -253,8 +253,18 @@ 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 +286,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 +353,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";
|
||||||
@@ -164,13 +162,16 @@ const String kOptionEnableConfirmClosingTabs = "enable-confirm-closing-tabs";
|
|||||||
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";
|
||||||
@@ -193,6 +194,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 =
|
||||||
@@ -325,10 +327,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;
|
||||||
@@ -440,7 +443,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";
|
||||||
@@ -452,6 +454,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
|
||||||
|
|||||||
@@ -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';
|
||||||
@@ -111,7 +110,6 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
buildPluginEntry(),
|
|
||||||
];
|
];
|
||||||
if (isIncomingOnly) {
|
if (isIncomingOnly) {
|
||||||
children.addAll([
|
children.addAll([
|
||||||
@@ -782,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());
|
||||||
@@ -890,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;
|
||||||
@@ -566,6 +563,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 +582,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 +611,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,
|
||||||
@@ -2255,51 +2271,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,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -182,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((_) {
|
||||||
@@ -274,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;
|
||||||
}
|
}
|
||||||
@@ -514,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) {
|
||||||
@@ -524,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();
|
||||||
@@ -836,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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
@@ -197,7 +197,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,
|
||||||
|
|||||||
@@ -127,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);
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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')),
|
||||||
|
|||||||
@@ -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,6 +9,7 @@ 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/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'
|
||||||
@@ -190,6 +192,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,
|
||||||
@@ -244,7 +247,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;
|
||||||
|
|||||||
@@ -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'];
|
||||||
@@ -904,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.
|
||||||
@@ -920,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
|
||||||
@@ -985,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(
|
||||||
@@ -2225,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
|
||||||
@@ -2273,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) {
|
||||||
@@ -2507,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.
|
||||||
@@ -2739,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();
|
||||||
@@ -2850,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();
|
||||||
|
|
||||||
@@ -3576,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') ==
|
||||||
@@ -4067,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');
|
||||||
@@ -4171,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';
|
||||||
@@ -171,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.
|
||||||
@@ -306,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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
91
flutter/lib/models/terminal_copy_shortcut.dart
Normal file
91
flutter/lib/models/terminal_copy_shortcut.dart
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:xterm/xterm.dart';
|
||||||
|
|
||||||
|
const _controlShiftVPasteShortcut = SingleActivator(
|
||||||
|
LogicalKeyboardKey.keyV,
|
||||||
|
control: true,
|
||||||
|
shift: true,
|
||||||
|
);
|
||||||
|
|
||||||
|
Future<void> writeTerminalClipboard(String text) async {
|
||||||
|
try {
|
||||||
|
await Clipboard.setData(ClipboardData(text: text));
|
||||||
|
} catch (error) {
|
||||||
|
debugPrint('[Terminal] Failed to write clipboard: $error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
235
flutter/lib/models/terminal_mouse_drag_reporter.dart
Normal file
235
flutter/lib/models/terminal_mouse_drag_reporter.dart
Normal file
@@ -0,0 +1,235 @@
|
|||||||
|
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;
|
||||||
|
|
||||||
|
bool handleDown(
|
||||||
|
PointerDownEvent event,
|
||||||
|
Terminal terminal,
|
||||||
|
TerminalViewState? terminalView,
|
||||||
|
) {
|
||||||
|
if (!_isPrimaryMouse(event) || !_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 = true;
|
||||||
|
_reporting = true;
|
||||||
|
controller.setSuspendPointerInput(true);
|
||||||
|
_clearSelection(controller);
|
||||||
|
final position = _cellAt(event, terminalView);
|
||||||
|
_lastReportedPosition = position;
|
||||||
|
terminal.textInput(
|
||||||
|
_report(terminal.mouseReportMode, position),
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool handleMove(
|
||||||
|
PointerMoveEvent event,
|
||||||
|
Terminal terminal,
|
||||||
|
TerminalViewState? terminalView,
|
||||||
|
) {
|
||||||
|
if (event.pointer != _pointerId) return false;
|
||||||
|
if (terminalView == null) {
|
||||||
|
cancel();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
final reportsDrag = _reportsDrag(terminal.mouseMode);
|
||||||
|
if (!_isPrimaryMouse(event)) {
|
||||||
|
if (_releasePending && reportsDrag) {
|
||||||
|
_reportRelease(
|
||||||
|
terminal,
|
||||||
|
_reporting ? _cellAt(event, terminalView) : _lastReportedPosition,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
cancel();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!_reporting || !reportsDrag) {
|
||||||
|
if (!reportsDrag) _releasePending = false;
|
||||||
|
_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);
|
||||||
|
_lastReportedPosition = 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,
|
||||||
|
) {
|
||||||
|
if (event.pointer != _pointerId) return false;
|
||||||
|
if (terminalView != null &&
|
||||||
|
_releasePending &&
|
||||||
|
_reportsDrag(terminal.mouseMode)) {
|
||||||
|
_reportRelease(
|
||||||
|
terminal,
|
||||||
|
_reporting ? _cellAt(event, terminalView) : _lastReportedPosition,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
_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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
CellOffset _cellAt(PointerEvent event, TerminalViewState terminalView) {
|
||||||
|
final renderTerminal = terminalView.renderTerminal;
|
||||||
|
return renderTerminal.getCellOffset(
|
||||||
|
renderTerminal.globalToLocal(event.position),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _isPrimaryMouse(PointerEvent event) =>
|
||||||
|
event.kind == PointerDeviceKind.mouse &&
|
||||||
|
(event.buttons & kPrimaryMouseButton) == kPrimaryMouseButton;
|
||||||
|
|
||||||
|
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,10 +1,19 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/gestures.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:xterm/xterm.dart';
|
import 'package:xterm/xterm.dart';
|
||||||
|
|
||||||
|
import 'terminal_copy_shortcut.dart';
|
||||||
|
import 'terminal_mouse_drag_reporter.dart';
|
||||||
|
|
||||||
/// xterm 4.0.0 encodes wheel buttons as 68..71; the extra bit reads as a Shift
|
/// 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.
|
/// modifier, so strict full-screen apps ignore the report and never scroll.
|
||||||
/// Upstream fix: TerminalStudio/xterm.dart#238.
|
/// Upstream fix: TerminalStudio/xterm.dart#238.
|
||||||
class WheelButtonFixMouseHandler implements TerminalMouseHandler {
|
class WheelButtonFixMouseHandler implements TerminalMouseHandler {
|
||||||
const WheelButtonFixMouseHandler();
|
const WheelButtonFixMouseHandler({this.positionProvider});
|
||||||
|
|
||||||
|
final CellOffset? Function()? positionProvider;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String? call(TerminalMouseEvent event) {
|
String? call(TerminalMouseEvent event) {
|
||||||
@@ -23,20 +32,270 @@ class WheelButtonFixMouseHandler implements TerminalMouseHandler {
|
|||||||
String _reportWheel(TerminalMouseEvent event) {
|
String _reportWheel(TerminalMouseEvent event) {
|
||||||
// Wheel buttons 4..7 go on the wire as 64..67, but `id` is 64 + 4..7.
|
// Wheel buttons 4..7 go on the wire as 64..67, but `id` is 64 + 4..7.
|
||||||
final button = event.button.id - 4;
|
final button = event.button.id - 4;
|
||||||
final x = event.position.x + 1;
|
final position = positionProvider?.call() ?? event.position;
|
||||||
final y = event.position.y + 1;
|
return encodeTerminalMouseReport(
|
||||||
switch (event.state.mouseReportMode) {
|
event.state.mouseReportMode,
|
||||||
case MouseReportMode.normal:
|
button,
|
||||||
case MouseReportMode.utf:
|
position,
|
||||||
final limit =
|
);
|
||||||
event.state.mouseReportMode == MouseReportMode.normal ? 223 : 2015;
|
}
|
||||||
final col = x > limit ? '\x00' : String.fromCharCode(32 + x);
|
}
|
||||||
final row = y > limit ? '\x00' : String.fromCharCode(32 + y);
|
|
||||||
return '\x1b[M${String.fromCharCode(32 + button)}$col$row';
|
class TerminalMouseInteraction extends StatefulWidget {
|
||||||
case MouseReportMode.sgr:
|
const TerminalMouseInteraction(
|
||||||
return '\x1b[<$button;$x;${y}M';
|
this.terminal, {
|
||||||
case MouseReportMode.urxvt:
|
super.key,
|
||||||
return '\x1b[${32 + button};$x;${y}M';
|
required this.controller,
|
||||||
}
|
this.focusNode,
|
||||||
|
this.backgroundOpacity = 1,
|
||||||
|
this.padding,
|
||||||
|
this.onSecondaryTapDown,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Terminal terminal;
|
||||||
|
final TerminalController controller;
|
||||||
|
final FocusNode? focusNode;
|
||||||
|
final double backgroundOpacity;
|
||||||
|
final EdgeInsets? padding;
|
||||||
|
final void Function(TapDownDetails, CellOffset)? onSecondaryTapDown;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<TerminalMouseInteraction> createState() =>
|
||||||
|
_TerminalMouseInteractionState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TerminalMouseInteractionState extends State<TerminalMouseInteraction> {
|
||||||
|
static const _selectionScrollInterval = Duration(milliseconds: 50);
|
||||||
|
static const _noScroll = 0;
|
||||||
|
static const _scrollUp = -1;
|
||||||
|
static const _scrollDown = 1;
|
||||||
|
|
||||||
|
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;
|
||||||
|
var _selectionHasScrolled = false;
|
||||||
|
var _scrollDirection = _noScroll;
|
||||||
|
TerminalViewState? get _terminalView => _terminalViewKey.currentState;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_mouseHandler = WheelButtonFixMouseHandler(
|
||||||
|
positionProvider: _cellAtPointer,
|
||||||
|
);
|
||||||
|
_installMouseHandler(widget.terminal);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(TerminalMouseInteraction oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
final terminalChanged = !identical(oldWidget.terminal, widget.terminal);
|
||||||
|
final controllerChanged =
|
||||||
|
!identical(oldWidget.controller, widget.controller);
|
||||||
|
if (!terminalChanged && !controllerChanged) return;
|
||||||
|
if (controllerChanged && !terminalChanged) {
|
||||||
|
_mouseDrag.updateController(widget.controller);
|
||||||
|
} else {
|
||||||
|
_mouseDrag.cancel();
|
||||||
|
}
|
||||||
|
_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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
if (_mouseDrag.handleDown(event, widget.terminal, _terminalView)) {
|
||||||
|
_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;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handlePointerMove(PointerMoveEvent event) {
|
||||||
|
_updatePointerPosition(event);
|
||||||
|
if (_mouseDrag.handleMove(event, widget.terminal, _terminalView)) 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);
|
||||||
|
if (!_mouseDrag.handleEnd(event, widget.terminal, _terminalView) &&
|
||||||
|
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() {
|
||||||
|
_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,
|
||||||
|
backgroundOpacity: widget.backgroundOpacity,
|
||||||
|
padding: widget.padding,
|
||||||
|
shortcuts: platformTerminalShortcuts(),
|
||||||
|
onKeyEvent: terminalCopyHandler(widget.terminal, widget.controller),
|
||||||
|
onSecondaryTapDown: widget.onSecondaryTapDown,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -251,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,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,301 +0,0 @@
|
|||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
import 'package:flutter_hbb/common.dart';
|
|
||||||
import 'package:flutter_hbb/models/model.dart';
|
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
import 'package:get/get.dart';
|
|
||||||
// to-do: do not depend on desktop
|
|
||||||
import 'package:flutter_hbb/desktop/widgets/remote_toolbar.dart';
|
|
||||||
import 'package:flutter_hbb/models/platform_model.dart';
|
|
||||||
|
|
||||||
import '../manager.dart';
|
|
||||||
import '../model.dart';
|
|
||||||
import '../common.dart';
|
|
||||||
|
|
||||||
// dup to flutter\lib\desktop\pages\desktop_setting_page.dart
|
|
||||||
const double _kCheckBoxLeftMargin = 10;
|
|
||||||
|
|
||||||
class LocationItem extends StatelessWidget {
|
|
||||||
final String peerId;
|
|
||||||
final FFI ffi;
|
|
||||||
final String location;
|
|
||||||
final LocationModel locationModel;
|
|
||||||
final bool isMenu;
|
|
||||||
|
|
||||||
LocationItem({
|
|
||||||
Key? key,
|
|
||||||
required this.peerId,
|
|
||||||
required this.ffi,
|
|
||||||
required this.location,
|
|
||||||
required this.locationModel,
|
|
||||||
required this.isMenu,
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
bool get isEmpty => locationModel.isEmpty;
|
|
||||||
|
|
||||||
static Widget createLocationItem(
|
|
||||||
String peerId, FFI ffi, String location, bool isMenu) {
|
|
||||||
final model = getLocationModel(location);
|
|
||||||
return model == null
|
|
||||||
? Container()
|
|
||||||
: LocationItem(
|
|
||||||
peerId: peerId,
|
|
||||||
ffi: ffi,
|
|
||||||
location: location,
|
|
||||||
locationModel: model,
|
|
||||||
isMenu: isMenu,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return ChangeNotifierProvider.value(
|
|
||||||
value: locationModel,
|
|
||||||
child: Consumer<LocationModel>(builder: (context, model, child) {
|
|
||||||
return Column(
|
|
||||||
children: model.pluginModels.entries
|
|
||||||
.map((entry) => _buildPluginItem(entry.key, entry.value))
|
|
||||||
.toList(),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildPluginItem(PluginId id, PluginModel model) => PluginItem(
|
|
||||||
pluginId: id,
|
|
||||||
peerId: peerId,
|
|
||||||
ffi: ffi,
|
|
||||||
location: location,
|
|
||||||
pluginModel: model,
|
|
||||||
isMenu: isMenu,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
class PluginItem extends StatelessWidget {
|
|
||||||
final PluginId pluginId;
|
|
||||||
final String peerId;
|
|
||||||
final FFI? ffi;
|
|
||||||
final String location;
|
|
||||||
final PluginModel pluginModel;
|
|
||||||
final bool isMenu;
|
|
||||||
|
|
||||||
PluginItem({
|
|
||||||
Key? key,
|
|
||||||
required this.pluginId,
|
|
||||||
required this.peerId,
|
|
||||||
this.ffi,
|
|
||||||
required this.location,
|
|
||||||
required this.pluginModel,
|
|
||||||
required this.isMenu,
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
bool get isEmpty => pluginModel.isEmpty;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return ChangeNotifierProvider.value(
|
|
||||||
value: pluginModel,
|
|
||||||
child: Consumer<PluginModel>(
|
|
||||||
builder: (context, pluginModel, child) {
|
|
||||||
return Column(
|
|
||||||
children: pluginModel.uiList.map((ui) => _buildItem(ui)).toList(),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildItem(UiType ui) {
|
|
||||||
Widget? child;
|
|
||||||
switch (ui.runtimeType) {
|
|
||||||
case UiButton:
|
|
||||||
if (isMenu) {
|
|
||||||
if (ffi != null) {
|
|
||||||
child = _buildMenuButton(ui as UiButton, ffi!);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
child = _buildButton(ui as UiButton);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case UiCheckbox:
|
|
||||||
if (isMenu) {
|
|
||||||
if (ffi != null) {
|
|
||||||
child = _buildCheckboxMenuButton(ui as UiCheckbox, ffi!);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
child = _buildCheckbox(ui as UiCheckbox);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
// to-do: add plugin icon and tooltip
|
|
||||||
return child ?? Container();
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildButton(UiButton ui) {
|
|
||||||
return TextButton(
|
|
||||||
onPressed: () => bind.pluginEvent(
|
|
||||||
id: pluginId,
|
|
||||||
peer: peerId,
|
|
||||||
event: _makeEvent(ui.key),
|
|
||||||
),
|
|
||||||
child: Text(ui.text),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildCheckbox(UiCheckbox ui) {
|
|
||||||
getChild(OptionModel model) {
|
|
||||||
final v = _getOption(model, ui.key);
|
|
||||||
if (v == null) {
|
|
||||||
// session or plugin not found
|
|
||||||
return Container();
|
|
||||||
}
|
|
||||||
|
|
||||||
onChanged(bool value) {
|
|
||||||
bind.pluginEvent(
|
|
||||||
id: pluginId,
|
|
||||||
peer: peerId,
|
|
||||||
event: _makeEvent(ui.key, v: value),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
final value = ConfigItem.isTrue(v);
|
|
||||||
return GestureDetector(
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Checkbox(
|
|
||||||
value: value,
|
|
||||||
onChanged: (_) => onChanged(!value),
|
|
||||||
).marginOnly(right: 5),
|
|
||||||
Expanded(
|
|
||||||
child: Text(translate(ui.text)),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
).marginOnly(left: _kCheckBoxLeftMargin),
|
|
||||||
onTap: () => onChanged(!value),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ChangeNotifierProvider.value(
|
|
||||||
value: getOptionModel(location, pluginId, peerId, ui.key),
|
|
||||||
child: Consumer<OptionModel>(
|
|
||||||
builder: (context, model, child) => getChild(model),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildCheckboxMenuButton(UiCheckbox ui, FFI ffi) {
|
|
||||||
getChild(OptionModel model) {
|
|
||||||
final v = _getOption(model, ui.key);
|
|
||||||
if (v == null) {
|
|
||||||
// session or plugin not found
|
|
||||||
return Container();
|
|
||||||
}
|
|
||||||
return CkbMenuButton(
|
|
||||||
value: ConfigItem.isTrue(v),
|
|
||||||
onChanged: (v) {
|
|
||||||
if (v != null) {
|
|
||||||
bind.pluginEvent(
|
|
||||||
id: pluginId,
|
|
||||||
peer: peerId,
|
|
||||||
event: _makeEvent(ui.key, v: v),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
// to-do: RustDesk translate or plugin translate ?
|
|
||||||
child: Text(ui.text),
|
|
||||||
ffi: ffi,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ChangeNotifierProvider.value(
|
|
||||||
value: getOptionModel(location, pluginId, peerId, ui.key),
|
|
||||||
child: Consumer<OptionModel>(
|
|
||||||
builder: (context, model, child) => getChild(model),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildMenuButton(UiButton ui, FFI ffi) {
|
|
||||||
return MenuButton(
|
|
||||||
onPressed: () => bind.pluginEvent(
|
|
||||||
id: pluginId,
|
|
||||||
peer: peerId,
|
|
||||||
event: _makeEvent(ui.key),
|
|
||||||
),
|
|
||||||
// to-do: support trailing icon, but it will cause tree shake error.
|
|
||||||
// ```
|
|
||||||
// This application cannot tree shake icons fonts. It has non-constant instances of IconData at the following locations:
|
|
||||||
// Target release_macos_bundle_flutter_assets failed: Exception: Avoid non-constant invocations of IconData or try to build again with --no-tree-shake-icons.
|
|
||||||
// ```
|
|
||||||
//
|
|
||||||
// trailingIcon: Icon(
|
|
||||||
// IconData(int.parse(ui.icon, radix: 16), fontFamily: 'MaterialIcons')),
|
|
||||||
//
|
|
||||||
// to-do: RustDesk translate or plugin translate ?
|
|
||||||
child: Text(ui.text),
|
|
||||||
ffi: ffi,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Uint8List _makeEvent(
|
|
||||||
String key, {
|
|
||||||
bool? v,
|
|
||||||
}) {
|
|
||||||
final event = MsgFromUi(
|
|
||||||
id: pluginId,
|
|
||||||
name: pluginManager.getPlugin(pluginId)?.meta.name ?? '',
|
|
||||||
location: location,
|
|
||||||
key: key,
|
|
||||||
value:
|
|
||||||
v != null ? (v ? ConfigItem.trueValue : ConfigItem.falseValue) : '',
|
|
||||||
action: '',
|
|
||||||
);
|
|
||||||
return Uint8List.fromList(event.toString().codeUnits);
|
|
||||||
}
|
|
||||||
|
|
||||||
String? _getOption(OptionModel model, String key) {
|
|
||||||
var v = model.value;
|
|
||||||
if (v == null) {
|
|
||||||
try {
|
|
||||||
if (peerId.isEmpty) {
|
|
||||||
v = bind.pluginGetSharedOption(id: pluginId, key: key);
|
|
||||||
} else {
|
|
||||||
v = bind.pluginGetSessionOption(id: pluginId, peer: peerId, key: key);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
debugPrint('Failed to get option "$key", $e');
|
|
||||||
v = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return v;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void handleReloading(Map<String, dynamic> evt) {
|
|
||||||
if (evt['id'] == null || evt['location'] == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
final uiList = <UiType>[];
|
|
||||||
for (var e in json.decode(evt['ui'] as String)) {
|
|
||||||
final ui = UiType.create(e);
|
|
||||||
if (ui != null) {
|
|
||||||
uiList.add(ui);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (uiList.isNotEmpty) {
|
|
||||||
addLocationUi(evt['location']!, evt['id']!, uiList);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
debugPrint('Failed handleReloading, json decode of ui, $e ');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void handleOption(Map<String, dynamic> evt) {
|
|
||||||
updateOption(
|
|
||||||
evt['location'], evt['id'], evt['peer'] ?? '', evt['key'], evt['value']);
|
|
||||||
}
|
|
||||||
@@ -1,202 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_hbb/common.dart';
|
|
||||||
import 'package:flutter_hbb/models/platform_model.dart';
|
|
||||||
import 'package:flutter_hbb/plugin/model.dart';
|
|
||||||
import 'package:flutter_hbb/plugin/common.dart';
|
|
||||||
import 'package:get/get.dart';
|
|
||||||
|
|
||||||
import '../manager.dart';
|
|
||||||
import './desc_ui.dart';
|
|
||||||
|
|
||||||
// to-do: use settings from desktop_setting_page.dart
|
|
||||||
const double _kCardFixedWidth = 540;
|
|
||||||
const double _kCardLeftMargin = 15;
|
|
||||||
const double _kContentHMargin = 15;
|
|
||||||
const double _kTitleFontSize = 20;
|
|
||||||
const double _kVersionFontSize = 12;
|
|
||||||
|
|
||||||
class DesktopSettingsCard extends StatefulWidget {
|
|
||||||
final PluginInfo plugin;
|
|
||||||
DesktopSettingsCard({
|
|
||||||
Key? key,
|
|
||||||
required this.plugin,
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<DesktopSettingsCard> createState() => _DesktopSettingsCardState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _DesktopSettingsCardState extends State<DesktopSettingsCard> {
|
|
||||||
PluginInfo get plugin => widget.plugin;
|
|
||||||
bool get installed => plugin.installed;
|
|
||||||
|
|
||||||
bool isEnabled = false;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
isEnabled = bind.pluginIsEnabled(id: plugin.meta.id);
|
|
||||||
return Row(
|
|
||||||
children: [
|
|
||||||
Flexible(
|
|
||||||
child: SizedBox(
|
|
||||||
width: _kCardFixedWidth,
|
|
||||||
child: Card(
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
header(),
|
|
||||||
body(),
|
|
||||||
],
|
|
||||||
).marginOnly(bottom: 10),
|
|
||||||
).marginOnly(left: _kCardLeftMargin, top: 15),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget header() {
|
|
||||||
return Row(
|
|
||||||
children: [
|
|
||||||
headerNameVersion(),
|
|
||||||
headerInstallEnable(),
|
|
||||||
],
|
|
||||||
).marginOnly(
|
|
||||||
left: _kContentHMargin,
|
|
||||||
top: 10,
|
|
||||||
bottom: 10,
|
|
||||||
right: _kContentHMargin,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget headerNameVersion() {
|
|
||||||
return Expanded(
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
widget.plugin.meta.name,
|
|
||||||
textAlign: TextAlign.start,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: _kTitleFontSize,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(
|
|
||||||
width: 5,
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
plugin.meta.version,
|
|
||||||
textAlign: TextAlign.start,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: _kVersionFontSize,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget headerButton(String label, VoidCallback onPressed) {
|
|
||||||
return Container(
|
|
||||||
child: ElevatedButton(
|
|
||||||
onPressed: onPressed,
|
|
||||||
child: Text(translate(label)),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget headerInstallEnable() {
|
|
||||||
final installButton = headerButton(
|
|
||||||
installed ? 'Uninstall' : 'Install',
|
|
||||||
() {
|
|
||||||
bind.pluginInstall(
|
|
||||||
id: plugin.meta.id,
|
|
||||||
b: !installed,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (installed) {
|
|
||||||
final updateButton = plugin.needUpdate
|
|
||||||
? headerButton('Update', () {
|
|
||||||
bind.pluginInstall(
|
|
||||||
id: plugin.meta.id,
|
|
||||||
b: !installed,
|
|
||||||
);
|
|
||||||
})
|
|
||||||
: Container();
|
|
||||||
|
|
||||||
final enableButton = !installed
|
|
||||||
? Container()
|
|
||||||
: headerButton(isEnabled ? 'Disable' : 'Enable', () {
|
|
||||||
if (isEnabled) {
|
|
||||||
clearPlugin(plugin.meta.id);
|
|
||||||
}
|
|
||||||
bind.pluginEnable(id: plugin.meta.id, v: !isEnabled);
|
|
||||||
setState(() {});
|
|
||||||
});
|
|
||||||
return Row(
|
|
||||||
children: [
|
|
||||||
updateButton,
|
|
||||||
SizedBox(
|
|
||||||
width: 10,
|
|
||||||
),
|
|
||||||
installButton,
|
|
||||||
SizedBox(
|
|
||||||
width: 10,
|
|
||||||
),
|
|
||||||
enableButton,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return installButton;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget body() {
|
|
||||||
return Column(children: [
|
|
||||||
author(),
|
|
||||||
description(),
|
|
||||||
more(),
|
|
||||||
]).marginOnly(
|
|
||||||
left: _kCardLeftMargin,
|
|
||||||
top: 4,
|
|
||||||
right: _kContentHMargin,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget author() {
|
|
||||||
return Align(
|
|
||||||
alignment: Alignment.centerLeft,
|
|
||||||
child: Text(plugin.meta.author),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget description() {
|
|
||||||
return Align(
|
|
||||||
alignment: Alignment.centerLeft,
|
|
||||||
child: Text(plugin.meta.description),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget more() {
|
|
||||||
if (!(installed && isEnabled)) {
|
|
||||||
return Container();
|
|
||||||
}
|
|
||||||
|
|
||||||
final List<Widget> children = [];
|
|
||||||
final model = getPluginModel(kLocationHostMainPlugin, plugin.meta.id);
|
|
||||||
if (model != null) {
|
|
||||||
children.add(PluginItem(
|
|
||||||
pluginId: plugin.meta.id,
|
|
||||||
peerId: '',
|
|
||||||
location: kLocationHostMainPlugin,
|
|
||||||
pluginModel: model,
|
|
||||||
isMenu: false,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
return ExpansionTile(
|
|
||||||
title: Text('Options'),
|
|
||||||
controlAffinity: ListTileControlAffinity.leading,
|
|
||||||
children: children,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -762,10 +762,6 @@ class RustdeskImpl {
|
|||||||
throw UnimplementedError("mainGetError");
|
throw UnimplementedError("mainGetError");
|
||||||
}
|
}
|
||||||
|
|
||||||
bool mainShowOption({required String key, dynamic hint}) {
|
|
||||||
throw UnimplementedError("mainShowOption");
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> mainSetOption(
|
Future<void> mainSetOption(
|
||||||
{required String key, required String value, dynamic hint}) {
|
{required String key, required String value, dynamic hint}) {
|
||||||
js.context.callMethod('setByName', [
|
js.context.callMethod('setByName', [
|
||||||
@@ -1377,6 +1373,10 @@ class RustdeskImpl {
|
|||||||
throw UnimplementedError("cmLoginRes");
|
throw UnimplementedError("cmLoginRes");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> cmCloseConnectionWindow({required int connId, dynamic hint}) {
|
||||||
|
throw UnimplementedError("cmCloseConnectionWindow");
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> cmCloseConnection({required int connId, dynamic hint}) {
|
Future<void> cmCloseConnection({required int connId, dynamic hint}) {
|
||||||
throw UnimplementedError("cmCloseConnection");
|
throw UnimplementedError("cmCloseConnection");
|
||||||
}
|
}
|
||||||
@@ -1644,78 +1644,6 @@ class RustdeskImpl {
|
|||||||
throw UnimplementedError("sendUrlScheme");
|
throw UnimplementedError("sendUrlScheme");
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> pluginEvent(
|
|
||||||
{required String id,
|
|
||||||
required String peer,
|
|
||||||
required Uint8List event,
|
|
||||||
dynamic hint}) {
|
|
||||||
throw UnimplementedError("pluginEvent");
|
|
||||||
}
|
|
||||||
|
|
||||||
Stream<EventToUI> pluginRegisterEventStream(
|
|
||||||
{required String id, dynamic hint}) {
|
|
||||||
throw UnimplementedError("pluginRegisterEventStream");
|
|
||||||
}
|
|
||||||
|
|
||||||
String? pluginGetSessionOption(
|
|
||||||
{required String id,
|
|
||||||
required String peer,
|
|
||||||
required String key,
|
|
||||||
dynamic hint}) {
|
|
||||||
throw UnimplementedError("pluginGetSessionOption");
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> pluginSetSessionOption(
|
|
||||||
{required String id,
|
|
||||||
required String peer,
|
|
||||||
required String key,
|
|
||||||
required String value,
|
|
||||||
dynamic hint}) {
|
|
||||||
throw UnimplementedError("pluginSetSessionOption");
|
|
||||||
}
|
|
||||||
|
|
||||||
String? pluginGetSharedOption(
|
|
||||||
{required String id, required String key, dynamic hint}) {
|
|
||||||
throw UnimplementedError("pluginGetSharedOption");
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> pluginSetSharedOption(
|
|
||||||
{required String id,
|
|
||||||
required String key,
|
|
||||||
required String value,
|
|
||||||
dynamic hint}) {
|
|
||||||
throw UnimplementedError("pluginSetSharedOption");
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> pluginReload({required String id, dynamic hint}) {
|
|
||||||
throw UnimplementedError("pluginReload");
|
|
||||||
}
|
|
||||||
|
|
||||||
void pluginEnable({required String id, required bool v, dynamic hint}) {
|
|
||||||
throw UnimplementedError("pluginEnable");
|
|
||||||
}
|
|
||||||
|
|
||||||
bool pluginIsEnabled({required String id, dynamic hint}) {
|
|
||||||
throw UnimplementedError("pluginIsEnabled");
|
|
||||||
}
|
|
||||||
|
|
||||||
bool pluginFeatureIsEnabled({dynamic hint}) {
|
|
||||||
throw UnimplementedError("pluginFeatureIsEnabled");
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> pluginSyncUi({required String syncTo, dynamic hint}) {
|
|
||||||
throw UnimplementedError("pluginSyncUi");
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> pluginListReload({dynamic hint}) {
|
|
||||||
throw UnimplementedError("pluginListReload");
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> pluginInstall(
|
|
||||||
{required String id, required bool b, dynamic hint}) {
|
|
||||||
throw UnimplementedError("pluginInstall");
|
|
||||||
}
|
|
||||||
|
|
||||||
bool isSupportMultiUiSession({required String version, dynamic hint}) {
|
bool isSupportMultiUiSession({required String version, dynamic hint}) {
|
||||||
return versionToNumber(v: version) > versionToNumber(v: '1.2.4');
|
return versionToNumber(v: version) > versionToNumber(v: '1.2.4');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import 'dart:js' as js;
|
import 'dart:js' as js;
|
||||||
import 'dart:html' as html;
|
|
||||||
// cycle imports, maybe we can improve this
|
// cycle imports, maybe we can improve this
|
||||||
import 'package:flutter_hbb/consts.dart';
|
import 'package:flutter_hbb/consts.dart';
|
||||||
|
|
||||||
@@ -13,8 +12,6 @@ final isWebDesktop_ = !js.context.callMethod('isMobile');
|
|||||||
|
|
||||||
final isDesktop_ = false;
|
final isDesktop_ = false;
|
||||||
|
|
||||||
String get screenInfo_ => js.context.callMethod('getByName', ['screen_info']);
|
|
||||||
|
|
||||||
final _localOs = js.context.callMethod('getByName', ['local_os', '']);
|
final _localOs = js.context.callMethod('getByName', ['local_os', '']);
|
||||||
final isWebOnWindows_ = _localOs == kPeerPlatformWindows;
|
final isWebOnWindows_ = _localOs == kPeerPlatformWindows;
|
||||||
final isWebOnLinux_ = _localOs == kPeerPlatformLinux;
|
final isWebOnLinux_ = _localOs == kPeerPlatformLinux;
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
abstract class NativeHandler {
|
|
||||||
bool onEvent(Map<String, dynamic> evt);
|
|
||||||
}
|
|
||||||
|
|
||||||
class NativeUiHandler extends NativeHandler {
|
|
||||||
NativeUiHandler._();
|
|
||||||
|
|
||||||
static NativeUiHandler instance = NativeUiHandler._();
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool onEvent(Map<String, dynamic> evt) {
|
|
||||||
throw UnimplementedError();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -36,8 +36,28 @@ class RelativeMouseState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class MainFlutterWindow: NSWindow {
|
class MainFlutterWindow: NSWindow {
|
||||||
|
private static let fullscreenWorkAreaSizes = NSMapTable<NSWindow, NSValue>(
|
||||||
|
keyOptions: [.weakMemory, .objectPointerPersonality],
|
||||||
|
valueOptions: .strongMemory
|
||||||
|
)
|
||||||
|
private static let fullscreenObserver = NotificationCenter.default.addObserver(
|
||||||
|
forName: NSWindow.willEnterFullScreenNotification,
|
||||||
|
object: nil,
|
||||||
|
queue: .main
|
||||||
|
) { notification in
|
||||||
|
guard let window = notification.object as? NSWindow,
|
||||||
|
let screen = window.screen else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fullscreenWorkAreaSizes.setObject(
|
||||||
|
NSValue(size: screen.visibleFrame.size),
|
||||||
|
forKey: window
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
override func awakeFromNib() {
|
override func awakeFromNib() {
|
||||||
rustdesk_core_main();
|
rustdesk_core_main();
|
||||||
|
_ = MainFlutterWindow.fullscreenObserver
|
||||||
let flutterViewController = FlutterViewController.init()
|
let flutterViewController = FlutterViewController.init()
|
||||||
let windowFrame = self.frame
|
let windowFrame = self.frame
|
||||||
self.contentViewController = flutterViewController
|
self.contentViewController = flutterViewController
|
||||||
@@ -278,6 +298,16 @@ class MainFlutterWindow: NSWindow {
|
|||||||
self.disableNativeRelativeMouseMode()
|
self.disableNativeRelativeMouseMode()
|
||||||
result(true)
|
result(true)
|
||||||
|
|
||||||
|
case "getMacOSWorkAreaSize":
|
||||||
|
guard Thread.isMainThread,
|
||||||
|
let window = registrar.view?.window,
|
||||||
|
let size = MainFlutterWindow.fullscreenWorkAreaSizes
|
||||||
|
.object(forKey: window)?.sizeValue else {
|
||||||
|
result(nil)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
result([Double(size.width), Double(size.height)])
|
||||||
|
|
||||||
default:
|
default:
|
||||||
result(FlutterMethodNotImplemented)
|
result(FlutterMethodNotImplemented)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -340,7 +340,7 @@ packages:
|
|||||||
description:
|
description:
|
||||||
path: "."
|
path: "."
|
||||||
ref: HEAD
|
ref: HEAD
|
||||||
resolved-ref: 533883bcb0ffe91a9afdb13b8bac9b14b3e054ba
|
resolved-ref: 8b774a66671cbb9bcb2631af6ac28f9bdd469ce3
|
||||||
url: "https://github.com/rustdesk-org/rustdesk_desktop_multi_window"
|
url: "https://github.com/rustdesk-org/rustdesk_desktop_multi_window"
|
||||||
source: git
|
source: git
|
||||||
version: "0.1.0"
|
version: "0.1.0"
|
||||||
@@ -409,14 +409,6 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "12.0.1"
|
version: "12.0.1"
|
||||||
external_path:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: external_path
|
|
||||||
sha256: "2095c626fbbefe70d5a4afc9b1137172a68ee2c276e51c3c1283394485bea8f4"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "1.0.3"
|
|
||||||
ffi:
|
ffi:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -529,11 +521,12 @@ packages:
|
|||||||
flutter_custom_cursor:
|
flutter_custom_cursor:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: flutter_custom_cursor
|
path: "."
|
||||||
sha256: "3850a32ac6de351ccc5e4286b6d94ff70c10abecd44479ea6c5aaea17264285d"
|
ref: db63b785c38153603e9fb84b50d3ec46f0d7e05e
|
||||||
url: "https://pub.dev"
|
resolved-ref: db63b785c38153603e9fb84b50d3ec46f0d7e05e
|
||||||
source: hosted
|
url: "https://github.com/rustdesk-org/flutter_custom_cursor"
|
||||||
version: "0.0.4"
|
source: git
|
||||||
|
version: "0.0.3"
|
||||||
flutter_gpu_texture_renderer:
|
flutter_gpu_texture_renderer:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -1597,9 +1590,9 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
path: "plugins/window_size"
|
path: "plugins/window_size"
|
||||||
ref: eb3964990cf19629c89ff8cb4a37640c7b3d5601
|
ref: "51e67ce047c72b26810b99e8473ddb44612fe356"
|
||||||
resolved-ref: eb3964990cf19629c89ff8cb4a37640c7b3d5601
|
resolved-ref: "51e67ce047c72b26810b99e8473ddb44612fe356"
|
||||||
url: "https://github.com/google/flutter-desktop-embedding.git"
|
url: "https://github.com/21pages/flutter-desktop-embedding.git"
|
||||||
source: git
|
source: git
|
||||||
version: "0.1.0"
|
version: "0.1.0"
|
||||||
xdg_directories:
|
xdg_directories:
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
|||||||
# Read more about iOS versioning at
|
# Read more about iOS versioning at
|
||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
# 1.1.9-1 works for android, but for ios it becomes 1.1.91, need to set it to 1.1.9-a.1 for iOS, will get 1.1.9.1, but iOS store not allow 4 numbers
|
# 1.1.9-1 works for android, but for ios it becomes 1.1.91, need to set it to 1.1.9-a.1 for iOS, will get 1.1.9.1, but iOS store not allow 4 numbers
|
||||||
version: 1.4.9+67
|
version: 1.5.0+68
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '^3.1.0'
|
sdk: '^3.1.0'
|
||||||
@@ -29,7 +29,6 @@ dependencies:
|
|||||||
|
|
||||||
ffi: ^2.1.0
|
ffi: ^2.1.0
|
||||||
path_provider: ^2.1.1
|
path_provider: ^2.1.1
|
||||||
external_path: ^1.0.3
|
|
||||||
provider: ^6.0.5
|
provider: ^6.0.5
|
||||||
tuple: ^2.0.0
|
tuple: ^2.0.0
|
||||||
wakelock_plus: ^1.1.3
|
wakelock_plus: ^1.1.3
|
||||||
@@ -58,12 +57,15 @@ dependencies:
|
|||||||
git:
|
git:
|
||||||
url: https://github.com/rustdesk-org/rustdesk_desktop_multi_window
|
url: https://github.com/rustdesk-org/rustdesk_desktop_multi_window
|
||||||
freezed_annotation: ^2.0.3
|
freezed_annotation: ^2.0.3
|
||||||
flutter_custom_cursor: ^0.0.4
|
flutter_custom_cursor:
|
||||||
|
git:
|
||||||
|
url: https://github.com/rustdesk-org/flutter_custom_cursor
|
||||||
|
ref: db63b785c38153603e9fb84b50d3ec46f0d7e05e
|
||||||
window_size:
|
window_size:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/google/flutter-desktop-embedding.git
|
url: https://github.com/21pages/flutter-desktop-embedding.git
|
||||||
path: plugins/window_size
|
path: plugins/window_size
|
||||||
ref: eb3964990cf19629c89ff8cb4a37640c7b3d5601
|
ref: 51e67ce047c72b26810b99e8473ddb44612fe356
|
||||||
get: ^4.6.5
|
get: ^4.6.5
|
||||||
visibility_detector: ^0.4.0+2
|
visibility_detector: ^0.4.0+2
|
||||||
contextmenu: ^3.0.0
|
contextmenu: ^3.0.0
|
||||||
|
|||||||
281
flutter/test/file_model_test.dart
Normal file
281
flutter/test/file_model_test.dart
Normal file
@@ -0,0 +1,281 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:flutter_hbb/models/file_model.dart';
|
||||||
|
import 'package:flutter_hbb/models/model.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
|
final _sessionId = UuidValue('00000000-0000-0000-0000-000000000000');
|
||||||
|
|
||||||
|
class _FakeFFI implements FFI {
|
||||||
|
@override
|
||||||
|
String id = 'test-peer';
|
||||||
|
@override
|
||||||
|
UuidValue get sessionId => _sessionId;
|
||||||
|
@override
|
||||||
|
late final FfiModel ffiModel = FfiModel(WeakReference(this));
|
||||||
|
|
||||||
|
@override
|
||||||
|
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
|
||||||
|
}
|
||||||
|
|
||||||
|
FileController _createController(FileFetcher fileFetcher) {
|
||||||
|
final ffi = _FakeFFI();
|
||||||
|
return FileController(
|
||||||
|
isLocal: false,
|
||||||
|
getSessionID: () => _sessionId,
|
||||||
|
rootState: WeakReference(ffi),
|
||||||
|
jobController: JobController(() => _sessionId, () => null),
|
||||||
|
fileFetcher: fileFetcher,
|
||||||
|
getOtherSideDirectoryData: () =>
|
||||||
|
DirectoryData(FileDirectory(), DirectoryOptions()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
FileDirectory _directory(String path) => FileDirectory()..path = path;
|
||||||
|
|
||||||
|
String _directoryJson(String path) => jsonEncode({
|
||||||
|
'id': 0,
|
||||||
|
'path': path,
|
||||||
|
'entries': <Object>[],
|
||||||
|
});
|
||||||
|
|
||||||
|
class _SentRead {
|
||||||
|
final String path;
|
||||||
|
final bool includeHidden;
|
||||||
|
|
||||||
|
const _SentRead(this.path, this.includeHidden);
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
test('a fast remote response is matched after registration', () async {
|
||||||
|
late final FileFetcher fileFetcher;
|
||||||
|
fileFetcher = FileFetcher(
|
||||||
|
() => _sessionId,
|
||||||
|
readRemoteDirectory: (_, path, __) {
|
||||||
|
fileFetcher.tryCompleteTask(_directoryJson(path), 'false');
|
||||||
|
return Future<void>.value();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
final directory = await fileFetcher.fetchDirectory('/fast', false, false);
|
||||||
|
expect(directory.path, '/fast');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a send failure fails and removes its registered task', () async {
|
||||||
|
final failure = StateError('send failed');
|
||||||
|
final fileFetcher = FileFetcher(
|
||||||
|
() => _sessionId,
|
||||||
|
readRemoteDirectory: (_, __, ___) => Future<void>.error(failure),
|
||||||
|
);
|
||||||
|
await expectLater(
|
||||||
|
fileFetcher.fetchDirectory('/failed', false, false),
|
||||||
|
throwsA(same(failure)),
|
||||||
|
);
|
||||||
|
expect(fileFetcher.hasPendingRemoteRead('/failed'), isFalse);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a resolved Home path completes the sole empty-path request', () async {
|
||||||
|
final sent = <_SentRead>[];
|
||||||
|
final fileFetcher = FileFetcher(
|
||||||
|
() => _sessionId,
|
||||||
|
readRemoteDirectory: (_, path, includeHidden) async {
|
||||||
|
sent.add(_SentRead(path, includeHidden));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
final controller = _createController(fileFetcher);
|
||||||
|
controller.directory.value = _directory('/initial');
|
||||||
|
|
||||||
|
final home = controller.openDirectory('');
|
||||||
|
await Future<void>.delayed(Duration.zero);
|
||||||
|
final response = _directoryJson('/home/user');
|
||||||
|
controller.initDirAndHome({'value': response});
|
||||||
|
expect(controller.homePath, '/home/user');
|
||||||
|
expect(controller.directory.value.path, '/initial');
|
||||||
|
fileFetcher.tryCompleteTask(response, 'false');
|
||||||
|
expect(await home, isTrue);
|
||||||
|
expect(controller.directory.value.path, '/home/user');
|
||||||
|
expect(sent.single.path, isEmpty);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('an automatic response initializes Home without a pending request', () {
|
||||||
|
final controller = _createController(FileFetcher(() => _sessionId));
|
||||||
|
|
||||||
|
controller.initDirAndHome({'value': _directoryJson('/home/user')});
|
||||||
|
|
||||||
|
expect(controller.homePath, '/home/user');
|
||||||
|
expect(controller.directory.value.path, '/home/user');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('an exact path response is not taken by a pending Home request',
|
||||||
|
() async {
|
||||||
|
final sent = <_SentRead>[];
|
||||||
|
final fileFetcher = FileFetcher(
|
||||||
|
() => _sessionId,
|
||||||
|
readRemoteDirectory: (_, path, includeHidden) async {
|
||||||
|
sent.add(_SentRead(path, includeHidden));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
final home = fileFetcher.fetchDirectory('', false, false);
|
||||||
|
final regular = fileFetcher.fetchDirectory('/regular', false, false);
|
||||||
|
await Future<void>.delayed(Duration.zero);
|
||||||
|
var homeCompleted = false;
|
||||||
|
home.then<void>((_) => homeCompleted = true);
|
||||||
|
|
||||||
|
fileFetcher.tryCompleteTask(_directoryJson('/unmatched'), 'false');
|
||||||
|
await Future<void>.delayed(Duration.zero);
|
||||||
|
expect(homeCompleted, isFalse);
|
||||||
|
|
||||||
|
fileFetcher.tryCompleteTask(_directoryJson('/regular'), 'false');
|
||||||
|
expect((await regular).path, '/regular');
|
||||||
|
await Future<void>.delayed(Duration.zero);
|
||||||
|
expect(homeCompleted, isFalse);
|
||||||
|
|
||||||
|
fileFetcher.tryCompleteTask(_directoryJson('/home/user'), 'false');
|
||||||
|
expect((await home).path, '/home/user');
|
||||||
|
expect(sent.map((request) => request.path), ['', '/regular']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a read error completes the sole pending request', () async {
|
||||||
|
final fileFetcher = FileFetcher(
|
||||||
|
() => _sessionId,
|
||||||
|
readRemoteDirectory: (_, __, ___) async {},
|
||||||
|
);
|
||||||
|
final request = fileFetcher.fetchDirectory('/denied', false, false);
|
||||||
|
await Future<void>.delayed(Duration.zero);
|
||||||
|
final expectation = expectLater(request, throwsA('permission denied'));
|
||||||
|
|
||||||
|
fileFetcher.tryCompleteRemoteTaskWithError('permission denied');
|
||||||
|
|
||||||
|
await expectation;
|
||||||
|
expect(fileFetcher.hasPendingRemoteRead('/denied'), isFalse);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('same-path requests share the pending read', () async {
|
||||||
|
final sent = <_SentRead>[];
|
||||||
|
late final FileFetcher fileFetcher;
|
||||||
|
fileFetcher = FileFetcher(
|
||||||
|
() => _sessionId,
|
||||||
|
readRemoteDirectory: (_, path, includeHidden) async {
|
||||||
|
sent.add(_SentRead(path, includeHidden));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
final controller = _createController(fileFetcher);
|
||||||
|
controller.directory.value = _directory('/initial');
|
||||||
|
|
||||||
|
final first = controller.openDirectory('/same');
|
||||||
|
final waiting = controller.openDirectory('/same');
|
||||||
|
|
||||||
|
await Future<void>.delayed(Duration.zero);
|
||||||
|
expect(sent.map((request) => request.path), ['/same']);
|
||||||
|
fileFetcher.tryCompleteTask(_directoryJson('/same'), 'false');
|
||||||
|
expect(await first, isTrue);
|
||||||
|
await Future<void>.delayed(Duration.zero);
|
||||||
|
|
||||||
|
expect(sent.map((request) => request.path), ['/same']);
|
||||||
|
expect(await waiting, isTrue);
|
||||||
|
expect(controller.directory.value.path, '/same');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('same-path requests with different hidden options are serialized',
|
||||||
|
() async {
|
||||||
|
final sent = <_SentRead>[];
|
||||||
|
final fileFetcher = FileFetcher(
|
||||||
|
() => _sessionId,
|
||||||
|
readRemoteDirectory: (_, path, includeHidden) async {
|
||||||
|
sent.add(_SentRead(path, includeHidden));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
final first = fileFetcher.fetchDirectory('/same', false, false);
|
||||||
|
final second = fileFetcher.fetchDirectory('/same', false, true);
|
||||||
|
|
||||||
|
await Future<void>.delayed(Duration.zero);
|
||||||
|
expect(sent.map((request) => request.includeHidden), [false]);
|
||||||
|
|
||||||
|
fileFetcher.tryCompleteTask(_directoryJson('/same'), 'false');
|
||||||
|
expect((await first).path, '/same');
|
||||||
|
await Future<void>.delayed(Duration.zero);
|
||||||
|
expect(sent.map((request) => request.includeHidden), [false, true]);
|
||||||
|
|
||||||
|
fileFetcher.tryCompleteTask(_directoryJson('/same'), 'false');
|
||||||
|
expect((await second).path, '/same');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('session invalidation cancels active and waiting reads', () async {
|
||||||
|
final sent = <_SentRead>[];
|
||||||
|
final fileFetcher = FileFetcher(
|
||||||
|
() => _sessionId,
|
||||||
|
readRemoteDirectory: (_, path, includeHidden) async {
|
||||||
|
sent.add(_SentRead(path, includeHidden));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
final first = fileFetcher.fetchDirectory('/same', false, false);
|
||||||
|
final waiting = fileFetcher.fetchDirectory('/same', false, true);
|
||||||
|
await Future<void>.delayed(Duration.zero);
|
||||||
|
final firstError = expectLater(first, throwsA(isA<StateError>()));
|
||||||
|
final waitingError = expectLater(waiting, throwsA(isA<StateError>()));
|
||||||
|
|
||||||
|
fileFetcher.beginRemoteSession();
|
||||||
|
|
||||||
|
await firstError;
|
||||||
|
await Future<void>.delayed(Duration.zero);
|
||||||
|
expect(sent.map((request) => request.includeHidden), [false]);
|
||||||
|
await waitingError;
|
||||||
|
expect(fileFetcher.hasPendingRemoteRead('/same'), isFalse);
|
||||||
|
final replacement = fileFetcher.fetchDirectory('/same', false, true);
|
||||||
|
await Future<void>.delayed(Duration.zero);
|
||||||
|
expect(sent.map((request) => request.includeHidden), [false, true]);
|
||||||
|
fileFetcher.tryCompleteTask(_directoryJson('/same'), 'false');
|
||||||
|
expect((await replacement).path, '/same');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a late dispatch failure cannot remove a replacement task', () async {
|
||||||
|
final dispatches = <Completer<void>>[];
|
||||||
|
final fileFetcher = FileFetcher(
|
||||||
|
() => _sessionId,
|
||||||
|
readRemoteDirectory: (_, __, ___) {
|
||||||
|
final dispatch = Completer<void>();
|
||||||
|
dispatches.add(dispatch);
|
||||||
|
return dispatch.future;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
final first = fileFetcher.fetchDirectory('/same', false, false);
|
||||||
|
await Future<void>.delayed(Duration.zero);
|
||||||
|
final firstError = expectLater(first, throwsA(isA<StateError>()));
|
||||||
|
fileFetcher.beginRemoteSession();
|
||||||
|
await firstError;
|
||||||
|
|
||||||
|
final replacement = fileFetcher.fetchDirectory('/same', false, false);
|
||||||
|
await Future<void>.delayed(Duration.zero);
|
||||||
|
expect(dispatches, hasLength(2));
|
||||||
|
dispatches.first.completeError(StateError('late dispatch failure'));
|
||||||
|
await Future<void>.delayed(Duration.zero);
|
||||||
|
|
||||||
|
expect(fileFetcher.hasPendingRemoteRead('/same'), isTrue);
|
||||||
|
fileFetcher.tryCompleteTask(_directoryJson('/same'), 'false');
|
||||||
|
expect((await replacement).path, '/same');
|
||||||
|
dispatches.last.complete();
|
||||||
|
await Future<void>.delayed(Duration.zero);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('navigation ignores stale directory responses', () async {
|
||||||
|
final fileFetcher = FileFetcher(
|
||||||
|
() => _sessionId,
|
||||||
|
readRemoteDirectory: (_, __, ___) async {},
|
||||||
|
);
|
||||||
|
final controller = _createController(fileFetcher);
|
||||||
|
controller.directory.value = _directory('/initial');
|
||||||
|
|
||||||
|
final stale = controller.openDirectory('/stale');
|
||||||
|
final latest = controller.openDirectory('/latest');
|
||||||
|
await Future<void>.delayed(Duration.zero);
|
||||||
|
|
||||||
|
fileFetcher.tryCompleteTask(_directoryJson('/latest'), 'false');
|
||||||
|
expect(await latest, isTrue);
|
||||||
|
fileFetcher.tryCompleteTask(_directoryJson('/stale'), 'false');
|
||||||
|
expect(await stale, isTrue);
|
||||||
|
|
||||||
|
expect(controller.directory.value.path, '/latest');
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -342,11 +342,43 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
group('shouldHandleTerminalPasteShortcut', () {
|
group('shouldHandleTerminalPasteShortcut', () {
|
||||||
|
test('handles only Ctrl+Shift+V on Linux with a virtual lock', () {
|
||||||
|
expect(
|
||||||
|
shouldHandleTerminalPasteShortcut(
|
||||||
|
platform: TargetPlatform.linux,
|
||||||
|
logicalKey: LogicalKeyboardKey.keyV,
|
||||||
|
isKeyDown: true,
|
||||||
|
isKeyRepeat: false,
|
||||||
|
controlPressed: true,
|
||||||
|
metaPressed: false,
|
||||||
|
altPressed: false,
|
||||||
|
shiftPressed: true,
|
||||||
|
modifierLockActive: true,
|
||||||
|
),
|
||||||
|
isTrue,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
shouldHandleTerminalPasteShortcut(
|
||||||
|
platform: TargetPlatform.linux,
|
||||||
|
logicalKey: LogicalKeyboardKey.keyV,
|
||||||
|
isKeyDown: true,
|
||||||
|
isKeyRepeat: false,
|
||||||
|
controlPressed: true,
|
||||||
|
metaPressed: false,
|
||||||
|
altPressed: false,
|
||||||
|
shiftPressed: false,
|
||||||
|
modifierLockActive: true,
|
||||||
|
),
|
||||||
|
isFalse,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test(
|
test(
|
||||||
'keeps default xterm paste behavior when virtual modifiers are inactive',
|
'keeps default xterm paste behavior when virtual modifiers are inactive',
|
||||||
() {
|
() {
|
||||||
expect(
|
expect(
|
||||||
shouldHandleTerminalPasteShortcut(
|
shouldHandleTerminalPasteShortcut(
|
||||||
|
platform: TargetPlatform.windows,
|
||||||
logicalKey: LogicalKeyboardKey.keyV,
|
logicalKey: LogicalKeyboardKey.keyV,
|
||||||
isKeyDown: true,
|
isKeyDown: true,
|
||||||
isKeyRepeat: false,
|
isKeyRepeat: false,
|
||||||
@@ -364,6 +396,7 @@ void main() {
|
|||||||
() {
|
() {
|
||||||
expect(
|
expect(
|
||||||
shouldHandleTerminalPasteShortcut(
|
shouldHandleTerminalPasteShortcut(
|
||||||
|
platform: TargetPlatform.windows,
|
||||||
logicalKey: LogicalKeyboardKey.keyV,
|
logicalKey: LogicalKeyboardKey.keyV,
|
||||||
isKeyDown: true,
|
isKeyDown: true,
|
||||||
isKeyRepeat: false,
|
isKeyRepeat: false,
|
||||||
@@ -377,6 +410,7 @@ void main() {
|
|||||||
);
|
);
|
||||||
expect(
|
expect(
|
||||||
shouldHandleTerminalPasteShortcut(
|
shouldHandleTerminalPasteShortcut(
|
||||||
|
platform: TargetPlatform.macOS,
|
||||||
logicalKey: LogicalKeyboardKey.keyV,
|
logicalKey: LogicalKeyboardKey.keyV,
|
||||||
isKeyDown: true,
|
isKeyDown: true,
|
||||||
isKeyRepeat: false,
|
isKeyRepeat: false,
|
||||||
@@ -393,6 +427,7 @@ void main() {
|
|||||||
test('handles paste shortcut repeats while a virtual lock is active', () {
|
test('handles paste shortcut repeats while a virtual lock is active', () {
|
||||||
expect(
|
expect(
|
||||||
shouldHandleTerminalPasteShortcut(
|
shouldHandleTerminalPasteShortcut(
|
||||||
|
platform: TargetPlatform.windows,
|
||||||
logicalKey: LogicalKeyboardKey.keyV,
|
logicalKey: LogicalKeyboardKey.keyV,
|
||||||
isKeyDown: false,
|
isKeyDown: false,
|
||||||
isKeyRepeat: true,
|
isKeyRepeat: true,
|
||||||
@@ -409,6 +444,7 @@ void main() {
|
|||||||
test('ignores key-up and unmodified V events', () {
|
test('ignores key-up and unmodified V events', () {
|
||||||
expect(
|
expect(
|
||||||
shouldHandleTerminalPasteShortcut(
|
shouldHandleTerminalPasteShortcut(
|
||||||
|
platform: TargetPlatform.windows,
|
||||||
logicalKey: LogicalKeyboardKey.keyV,
|
logicalKey: LogicalKeyboardKey.keyV,
|
||||||
isKeyDown: false,
|
isKeyDown: false,
|
||||||
isKeyRepeat: false,
|
isKeyRepeat: false,
|
||||||
@@ -422,6 +458,7 @@ void main() {
|
|||||||
);
|
);
|
||||||
expect(
|
expect(
|
||||||
shouldHandleTerminalPasteShortcut(
|
shouldHandleTerminalPasteShortcut(
|
||||||
|
platform: TargetPlatform.windows,
|
||||||
logicalKey: LogicalKeyboardKey.keyV,
|
logicalKey: LogicalKeyboardKey.keyV,
|
||||||
isKeyDown: true,
|
isKeyDown: true,
|
||||||
isKeyRepeat: false,
|
isKeyRepeat: false,
|
||||||
@@ -444,6 +481,7 @@ void main() {
|
|||||||
]) {
|
]) {
|
||||||
expect(
|
expect(
|
||||||
shouldHandleTerminalPasteShortcut(
|
shouldHandleTerminalPasteShortcut(
|
||||||
|
platform: TargetPlatform.windows,
|
||||||
logicalKey: LogicalKeyboardKey.keyV,
|
logicalKey: LogicalKeyboardKey.keyV,
|
||||||
isKeyDown: true,
|
isKeyDown: true,
|
||||||
isKeyRepeat: false,
|
isKeyRepeat: false,
|
||||||
@@ -461,6 +499,7 @@ void main() {
|
|||||||
test('ignores non-V key events', () {
|
test('ignores non-V key events', () {
|
||||||
expect(
|
expect(
|
||||||
shouldHandleTerminalPasteShortcut(
|
shouldHandleTerminalPasteShortcut(
|
||||||
|
platform: TargetPlatform.windows,
|
||||||
logicalKey: LogicalKeyboardKey.keyC,
|
logicalKey: LogicalKeyboardKey.keyC,
|
||||||
isKeyDown: true,
|
isKeyDown: true,
|
||||||
isKeyRepeat: false,
|
isKeyRepeat: false,
|
||||||
|
|||||||
@@ -1,7 +1,35 @@
|
|||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter_hbb/models/terminal_mouse_handler.dart';
|
import 'package:flutter_hbb/models/terminal_mouse_handler.dart';
|
||||||
|
import 'package:flutter/gestures.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:xterm/xterm.dart';
|
import 'package:xterm/xterm.dart';
|
||||||
|
|
||||||
|
const _terminalSize = Size(400, 120);
|
||||||
|
|
||||||
|
Widget _terminalHarness(
|
||||||
|
Terminal terminal,
|
||||||
|
TerminalController controller,
|
||||||
|
) =>
|
||||||
|
MaterialApp(
|
||||||
|
home: Align(
|
||||||
|
alignment: Alignment.topLeft,
|
||||||
|
child: SizedBox(
|
||||||
|
width: _terminalSize.width,
|
||||||
|
height: _terminalSize.height,
|
||||||
|
child: TerminalMouseInteraction(
|
||||||
|
terminal,
|
||||||
|
controller: controller,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
void _writeLines(Terminal terminal, int count) => terminal.write(
|
||||||
|
List.generate(count, (index) => 'line $index\r\n').join(),
|
||||||
|
);
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
late Terminal terminal;
|
late Terminal terminal;
|
||||||
late List<String> output;
|
late List<String> output;
|
||||||
@@ -12,6 +40,34 @@ void main() {
|
|||||||
..onOutput = output.add;
|
..onOutput = output.add;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('Linux Ctrl+V is not paste', (tester) async {
|
||||||
|
debugDefaultTargetPlatformOverride = TargetPlatform.linux;
|
||||||
|
try {
|
||||||
|
final messenger = tester.binding.defaultBinaryMessenger;
|
||||||
|
messenger.setMockMethodCallHandler(
|
||||||
|
SystemChannels.platform,
|
||||||
|
(_) async => {'text': 'clipboard'},
|
||||||
|
);
|
||||||
|
final controller = TerminalController();
|
||||||
|
addTearDown(controller.dispose);
|
||||||
|
await tester.pumpWidget(_terminalHarness(terminal, controller));
|
||||||
|
await tester.tap(find.byType(TerminalView));
|
||||||
|
await tester.pump(kDoubleTapTimeout);
|
||||||
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
|
||||||
|
await tester.sendKeyEvent(LogicalKeyboardKey.keyV);
|
||||||
|
final controlVOutput = List.of(output);
|
||||||
|
output.clear();
|
||||||
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft);
|
||||||
|
await tester.sendKeyEvent(LogicalKeyboardKey.keyV);
|
||||||
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.shiftLeft);
|
||||||
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
|
||||||
|
expect(controlVOutput, ['\x16']);
|
||||||
|
expect(output, ['clipboard']);
|
||||||
|
} finally {
|
||||||
|
debugDefaultTargetPlatformOverride = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
String? report(
|
String? report(
|
||||||
TerminalMouseButton button, [
|
TerminalMouseButton button, [
|
||||||
TerminalMouseButtonState state = TerminalMouseButtonState.down,
|
TerminalMouseButtonState state = TerminalMouseButtonState.down,
|
||||||
@@ -111,4 +167,133 @@ void main() {
|
|||||||
isNull,
|
isNull,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('dragging below scrolls and extends selection', (tester) async {
|
||||||
|
final controller = TerminalController();
|
||||||
|
_writeLines(terminal, 80);
|
||||||
|
await tester.pumpWidget(_terminalHarness(terminal, controller));
|
||||||
|
final terminalView =
|
||||||
|
tester.state<TerminalViewState>(find.byType(TerminalView));
|
||||||
|
final scrollController = terminalView.widget.scrollController!;
|
||||||
|
scrollController.jumpTo(0);
|
||||||
|
await tester.pump();
|
||||||
|
final renderTerminal = terminalView.renderTerminal;
|
||||||
|
const localStart = Offset(20, 20);
|
||||||
|
final startCell = renderTerminal.getCellOffset(localStart);
|
||||||
|
final mouse = TestPointer(1, PointerDeviceKind.mouse);
|
||||||
|
final outside = Offset(20, renderTerminal.size.height);
|
||||||
|
await tester.handlePointerEventRecord([
|
||||||
|
PointerEventRecord(Duration.zero, [
|
||||||
|
mouse.down(renderTerminal.localToGlobal(localStart)),
|
||||||
|
mouse.move(renderTerminal.localToGlobal(outside)),
|
||||||
|
]),
|
||||||
|
PointerEventRecord(const Duration(milliseconds: 150), [
|
||||||
|
mouse.move(
|
||||||
|
renderTerminal.localToGlobal(outside + const Offset(1, 1)),
|
||||||
|
),
|
||||||
|
mouse.up(),
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(scrollController.offset, greaterThan(0));
|
||||||
|
expect(controller.selection!.begin, startCell);
|
||||||
|
expect(controller.selection!.end.y, greaterThan(startCell.y));
|
||||||
|
final releasedOffset = scrollController.offset;
|
||||||
|
await tester.pump(const Duration(milliseconds: 100));
|
||||||
|
expect(scrollController.offset, releasedOffset);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('tmux mouse input is reported without local selection',
|
||||||
|
(tester) async {
|
||||||
|
final controller = TerminalController();
|
||||||
|
terminal.write('\x1b[?1049h\x1b[?1002h\x1b[?1006hword');
|
||||||
|
await tester.pumpWidget(_terminalHarness(terminal, controller));
|
||||||
|
final renderTerminal = tester
|
||||||
|
.state<TerminalViewState>(find.byType(TerminalView))
|
||||||
|
.renderTerminal;
|
||||||
|
const wheel = Offset(120, 40);
|
||||||
|
await tester.sendEventToBinding(
|
||||||
|
PointerScrollEvent(
|
||||||
|
position: renderTerminal.localToGlobal(wheel),
|
||||||
|
scrollDelta: const Offset(0, 40),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.pump();
|
||||||
|
final wheelCell = renderTerminal.getCellOffset(wheel);
|
||||||
|
expect(output.first, '\x1b[<65;${wheelCell.x + 1};${wheelCell.y + 1}M');
|
||||||
|
output.clear();
|
||||||
|
final clickPosition =
|
||||||
|
renderTerminal.getOffset(const CellOffset(0, 0)) + const Offset(1, 1);
|
||||||
|
final clickCell = renderTerminal.getCellOffset(clickPosition);
|
||||||
|
final mouse = await tester.createGesture(kind: PointerDeviceKind.mouse);
|
||||||
|
await mouse.down(renderTerminal.localToGlobal(clickPosition));
|
||||||
|
await mouse.up();
|
||||||
|
await tester.pump();
|
||||||
|
await mouse.down(renderTerminal.localToGlobal(clickPosition));
|
||||||
|
await mouse.up();
|
||||||
|
await tester.pump(kDoubleTapTimeout);
|
||||||
|
expect(output, [
|
||||||
|
'\x1b[<0;${clickCell.x + 1};${clickCell.y + 1}M',
|
||||||
|
'\x1b[<0;${clickCell.x + 1};${clickCell.y + 1}m',
|
||||||
|
'\x1b[<0;${clickCell.x + 1};${clickCell.y + 1}M',
|
||||||
|
'\x1b[<0;${clickCell.x + 1};${clickCell.y + 1}m',
|
||||||
|
]);
|
||||||
|
expect(controller.selection, isNull);
|
||||||
|
expect(controller.suspendedPointerInputs, isFalse);
|
||||||
|
output.clear();
|
||||||
|
const start = Offset(40, 40);
|
||||||
|
const end = Offset(240, 80);
|
||||||
|
final startCell = renderTerminal.getCellOffset(start);
|
||||||
|
final endCell = renderTerminal.getCellOffset(end);
|
||||||
|
await mouse.down(renderTerminal.localToGlobal(start));
|
||||||
|
await mouse.moveTo(renderTerminal.localToGlobal(end));
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
expect(output, [
|
||||||
|
'\x1b[<0;${startCell.x + 1};${startCell.y + 1}M',
|
||||||
|
'\x1b[<32;${endCell.x + 1};${endCell.y + 1}M',
|
||||||
|
]);
|
||||||
|
expect(controller.selection, isNull);
|
||||||
|
await mouse.up();
|
||||||
|
expect(output.last, '\x1b[<0;${endCell.x + 1};${endCell.y + 1}m');
|
||||||
|
expect(controller.suspendedPointerInputs, isFalse);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('tmux drag stays suppressed after mouse mode is disabled',
|
||||||
|
(tester) async {
|
||||||
|
final controller = TerminalController();
|
||||||
|
terminal.write('\x1b[?1049h\x1b[?1002h\x1b[?1006hword');
|
||||||
|
await tester.pumpWidget(_terminalHarness(terminal, controller));
|
||||||
|
final renderTerminal = tester
|
||||||
|
.state<TerminalViewState>(find.byType(TerminalView))
|
||||||
|
.renderTerminal;
|
||||||
|
final mouse = await tester.createGesture(kind: PointerDeviceKind.mouse);
|
||||||
|
final start = renderTerminal.localToGlobal(const Offset(40, 40));
|
||||||
|
final end = renderTerminal.localToGlobal(const Offset(240, 80));
|
||||||
|
await mouse.down(start);
|
||||||
|
output.clear();
|
||||||
|
terminal.write('\x1b[?1002l');
|
||||||
|
await mouse.moveTo(end);
|
||||||
|
|
||||||
|
expect(output, isEmpty);
|
||||||
|
expect(controller.selection, isNull);
|
||||||
|
expect(controller.suspendedPointerInputs, isTrue);
|
||||||
|
terminal.write('\x1b[?1002h');
|
||||||
|
await mouse.moveTo(start);
|
||||||
|
expect(output, isEmpty);
|
||||||
|
expect(controller.selection, isNull);
|
||||||
|
await mouse.up();
|
||||||
|
expect(output, isEmpty);
|
||||||
|
expect(controller.suspendedPointerInputs, isFalse);
|
||||||
|
|
||||||
|
await mouse.down(start);
|
||||||
|
output.clear();
|
||||||
|
terminal.write('\x1b[?1002l');
|
||||||
|
await mouse.up();
|
||||||
|
await tester.pump(kDoubleTapTimeout);
|
||||||
|
|
||||||
|
expect(output, isEmpty);
|
||||||
|
expect(controller.selection, isNull);
|
||||||
|
expect(controller.suspendedPointerInputs, isFalse);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,6 +42,13 @@ impl Enigo {
|
|||||||
&mut self.custom_mouse
|
&mut self.custom_mouse
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Override the display server guessed in `Default::default`: on "x11" every method here
|
||||||
|
/// routes to `xdo`, and a null xdo context makes all of them silent no-ops. A caller
|
||||||
|
/// installing custom devices knows better than the guess.
|
||||||
|
pub fn set_is_x11(&mut self, is_x11: bool) {
|
||||||
|
self.is_x11 = is_x11;
|
||||||
|
}
|
||||||
|
|
||||||
/// Clear remapped keycodes
|
/// Clear remapped keycodes
|
||||||
pub fn tfc_clear_remapped(&mut self) {
|
pub fn tfc_clear_remapped(&mut self) {
|
||||||
if let Some(tfc) = &mut self.tfc {
|
if let Some(tfc) = &mut self.tfc {
|
||||||
@@ -390,3 +397,52 @@ fn test_key_seq() {
|
|||||||
let mut en = Enigo::new();
|
let mut en = Enigo::new();
|
||||||
en.key_sequence("^^");
|
en.key_sequence("^^");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Both directions: the failure is silent, so a one-directional test passes against the bug.
|
||||||
|
#[test]
|
||||||
|
fn test_custom_mouse_dispatch_follows_is_x11() {
|
||||||
|
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
struct CountingMouse(Arc<AtomicUsize>);
|
||||||
|
impl MouseControllable for CountingMouse {
|
||||||
|
fn as_any(&self) -> &dyn std::any::Any {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
fn as_mut_any(&mut self) -> &mut dyn std::any::Any {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
fn mouse_move_to(&mut self, _x: i32, _y: i32) {
|
||||||
|
self.0.fetch_add(1, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
fn mouse_move_relative(&mut self, _x: i32, _y: i32) {}
|
||||||
|
fn mouse_down(&mut self, _button: MouseButton) -> crate::ResultType {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
fn mouse_up(&mut self, _button: MouseButton) {}
|
||||||
|
fn mouse_click(&mut self, _button: MouseButton) {}
|
||||||
|
fn mouse_scroll_x(&mut self, _length: i32) {}
|
||||||
|
fn mouse_scroll_y(&mut self, _length: i32) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
let calls = Arc::new(AtomicUsize::new(0));
|
||||||
|
let mut en = Enigo::new();
|
||||||
|
en.set_custom_mouse(Box::new(CountingMouse(calls.clone())));
|
||||||
|
|
||||||
|
en.set_is_x11(false);
|
||||||
|
en.mouse_move_to(10, 20);
|
||||||
|
assert_eq!(
|
||||||
|
calls.load(Ordering::Relaxed),
|
||||||
|
1,
|
||||||
|
"custom mouse was not reached on the non-x11 branch"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Negative control: on the x11 branch the custom device must be bypassed entirely.
|
||||||
|
en.set_is_x11(true);
|
||||||
|
en.mouse_move_to(30, 40);
|
||||||
|
assert_eq!(
|
||||||
|
calls.load(Ordering::Relaxed),
|
||||||
|
1,
|
||||||
|
"custom mouse was reached on the x11 branch"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user