1301 Commits

480 changed files with 48180 additions and 15717 deletions

View File

@@ -1,8 +1,14 @@
[target.x86_64-pc-windows-msvc] [target.x86_64-pc-windows-msvc]
rustflags = ["-Ctarget-feature=+crt-static"] rustflags = ["-Ctarget-feature=+crt-static"]
[target.i686-pc-windows-msvc] [target.i686-pc-windows-msvc]
rustflags = ["-Ctarget-feature=+crt-static"] rustflags = ["-C", "target-feature=+crt-static", "-C", "link-args=/NODEFAULTLIB:MSVCRT"]
[target.'cfg(target_os="macos")'] [target.'cfg(target_os="macos")']
rustflags = [ rustflags = [
"-C", "link-args=-sectcreate __CGPreLoginApp __cgpreloginapp /dev/null", "-C", "link-args=-sectcreate __CGPreLoginApp __cgpreloginapp /dev/null",
] ]
#[target.'cfg(target_os="linux")']
# glibc-static required, this may fix https://github.com/rustdesk/rustdesk/issues/9103, but I do not want this big change
# this is unlikely to help also, because the other so files still use libc dynamically
#rustflags = [
# "-C", "link-args=-Wl,-Bstatic -lc -Wl,-Bdynamic"
#]

View File

@@ -6,8 +6,9 @@ on:
workflow_call: workflow_call:
env: env:
FLUTTER_VERSION: "3.16.9" FLUTTER_VERSION: "3.19.6"
FLUTTER_RUST_BRIDGE_VERSION: "1.80.1" FLUTTER_RUST_BRIDGE_VERSION: "1.80.1"
RUST_VERSION: "1.75" # https://github.com/rustdesk/rustdesk/discussions/7503
jobs: jobs:
generate_bridge: generate_bridge:
@@ -37,10 +38,8 @@ jobs:
git \ git \
g++ \ g++ \
libclang-10-dev \ libclang-10-dev \
libclang-dev \
libgtk-3-dev \ libgtk-3-dev \
llvm-10-dev \ llvm-10-dev \
llvm-dev \
nasm \ nasm \
ninja-build \ ninja-build \
pkg-config \ pkg-config \
@@ -49,9 +48,9 @@ jobs:
- name: Install Rust toolchain - name: Install Rust toolchain
uses: dtolnay/rust-toolchain@v1 uses: dtolnay/rust-toolchain@v1
with: with:
toolchain: stable toolchain: ${{ env.RUST_VERSION }}
targets: ${{ matrix.job.target }} targets: ${{ matrix.job.target }}
components: '' components: "rustfmt"
- uses: Swatinem/rust-cache@v2 - uses: Swatinem/rust-cache@v2
with: with:

View File

@@ -4,9 +4,9 @@ env:
# MIN_SUPPORTED_RUST_VERSION: "1.46.0" # MIN_SUPPORTED_RUST_VERSION: "1.46.0"
# CICD_INTERMEDIATES_DIR: "_cicd-intermediates" # CICD_INTERMEDIATES_DIR: "_cicd-intermediates"
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite" VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
# vcpkg version: 2023.10.19 # vcpkg version: 2024.06.15
# for multiarch gcc compatibility # for multiarch gcc compatibility
VCPKG_COMMIT_ID: "8eb57355a4ffb410a2e94c07b4dca2dffbee8e50" VCPKG_COMMIT_ID: "f7423ee180c4b7f40d43402c2feb3859161ef625"
on: on:
workflow_dispatch: workflow_dispatch:
@@ -21,6 +21,9 @@ on:
- ".github/**" - ".github/**"
- "docs/**" - "docs/**"
- "README.md" - "README.md"
- "res/**"
- "appimage/**"
- "flatpak/**"
jobs: jobs:
# ensure_cargo_fmt: # ensure_cargo_fmt:
@@ -71,12 +74,12 @@ jobs:
# - { target: aarch64-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true } # - { target: aarch64-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true }
# - { target: arm-unknown-linux-gnueabihf , os: ubuntu-20.04, use-cross: true } # - { target: arm-unknown-linux-gnueabihf , os: ubuntu-20.04, use-cross: true }
# - { target: arm-unknown-linux-musleabihf, os: ubuntu-20.04, use-cross: true } # - { target: arm-unknown-linux-musleabihf, os: ubuntu-20.04, use-cross: true }
# - { target: i686-pc-windows-msvc , os: windows-2019 } # - { target: i686-pc-windows-msvc , os: windows-2022 }
# - { target: i686-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true } # - { target: i686-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true }
# - { target: i686-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } # - { target: i686-unknown-linux-musl , os: ubuntu-20.04, use-cross: true }
# - { target: x86_64-apple-darwin , os: macos-10.15 } # - { target: x86_64-apple-darwin , os: macos-10.15 }
# - { target: x86_64-pc-windows-gnu , os: windows-2019 } # - { target: x86_64-pc-windows-gnu , os: windows-2022 }
# - { target: x86_64-pc-windows-msvc , os: windows-2019 } # - { target: x86_64-pc-windows-msvc , os: windows-2022 }
- { target: x86_64-unknown-linux-gnu , os: ubuntu-20.04 } - { target: x86_64-unknown-linux-gnu , os: ubuntu-20.04 }
# - { target: x86_64-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } # - { target: x86_64-unknown-linux-musl , os: ubuntu-20.04, use-cross: true }
steps: steps:
@@ -103,11 +106,14 @@ jobs:
gcc \ gcc \
git \ git \
g++ \ g++ \
libpam0g-dev \
libasound2-dev \ libasound2-dev \
libgstreamer1.0-dev \ libgstreamer1.0-dev \
libgstreamer-plugins-base1.0-dev \ libgstreamer-plugins-base1.0-dev \
libgtk-3-dev \ libgtk-3-dev \
libpulse-dev \ libpulse-dev \
libva-dev \
libvdpau-dev \
libxcb-randr0-dev \ libxcb-randr0-dev \
libxcb-shape0-dev \ libxcb-shape0-dev \
libxcb-xfixes0-dev \ libxcb-xfixes0-dev \

37
.github/workflows/clear-cache.yml vendored Normal file
View File

@@ -0,0 +1,37 @@
name: Clear cache
on:
workflow_dispatch:
permissions:
actions: write
jobs:
clear-cache:
runs-on: ubuntu-latest
steps:
- name: Clear cache
uses: actions/github-script@v7
with:
script: |
console.log("About to clear")
const caches = await github.rest.actions.getActionsCacheList({
owner: context.repo.owner,
repo: context.repo.repo,
})
for (const cache of caches.data.actions_caches) {
console.log(cache)
github.rest.actions.deleteActionsCacheById({
owner: context.repo.owner,
repo: context.repo.repo,
cache_id: cache.id,
})
}
console.log("Clear completed")
- name: Purge cache # Above seems not clear thouroughly, so add this to double clear
uses: MyAlbum/purge-cache@v2
with:
accessed: true # Purge caches by their last accessed time (default)
created: false # Purge caches by their created time (default)
max-age: 1 # in seconds

39
.github/workflows/fdroid.yml vendored Normal file
View File

@@ -0,0 +1,39 @@
name: Fdroid version file generation
on:
workflow_dispatch:
push:
tags:
- 'v[0-9]+.[0-9]+.[0-9]+'
- '[0-9]+.[0-9]+.[0-9]+'
- 'v[0-9]+.[0-9]+.[0-9]+-[0-9]+'
- '[0-9]+.[0-9]+.[0-9]+-[0-9]+'
jobs:
# https://gitlab.com/fdroid/fdroiddata/-/blob/master/metadata/com.carriez.flutter_hbb.yml
# Finds latest release and transforms F-Droid version code from version as follows:
# X.Y.Z-A => X * 1e6 + Y * 1e4 + Z * 1e2 + A
update-fdroid-version-file:
name: Publish RustDesk version file for F-Droid updater
runs-on: ubuntu-latest
steps:
- name: Generate RustDesk version file
run: |
if [ "${GITHUB_REF_TYPE}" = "tag" ]; then
UPSTREAM_VERNAME="${GITHUB_REF##refs/tags/}"
UPSTREAM_VERNAME="${UPSTREAM_VERNAME##v}"
else
UPSTREAM_VERNAME="$(curl https://api.github.com/repos/rustdesk/rustdesk/releases/latest | jq -r .tag_name | sed 's/^v//')"
fi
UPSTREAM_VERCODE="$(echo "$UPSTREAM_VERNAME" | tr '.' ' ' | tr '-' ' ' | while read -r MAJOR MINOR PATCH REV; do [ -z "$MAJOR" ] && MAJOR=0; [ -z "$MINOR" ] && MINOR=0; [ -z "$PATCH" ] && PATCH=0; [ -z "$REV" ] && REV=0; echo "$(( 1000000 * $MAJOR + 10000 * $MINOR + 100 * $PATCH + $REV ))"; done)"
echo "versionName=$UPSTREAM_VERNAME" > rustdesk-version.txt
echo "versionCode=$UPSTREAM_VERCODE" >> rustdesk-version.txt
shell: bash
- name: Publish RustDesk version file
uses: softprops/action-gh-release@v1
with:
prerelease: true
tag_name: "fdroid-version"
files: |
./rustdesk-version.txt

File diff suppressed because it is too large Load Diff

View File

@@ -13,6 +13,9 @@ on:
- ".github/**" - ".github/**"
- "docs/**" - "docs/**"
- "README.md" - "README.md"
- "res/**"
- "appimage/**"
- "flatpak/**"
jobs: jobs:
run-ci: run-ci:

View File

@@ -15,24 +15,4 @@ jobs:
secrets: inherit secrets: inherit
with: with:
upload-artifact: true upload-artifact: true
upload-tag: ${{ env.GITHUB_REF_NAME }} upload-tag: ${{ github.ref_name }}
update-fdroid-version-file:
name: Publish RustDesk version file for F-Droid updater
runs-on: ubuntu-latest
steps:
- name: Generate RustDesk version file
run: |
UPSTREAM_VERNAME="$GITHUB_REF_NAME"
UPSTREAM_VERCODE="$(echo "$UPSTREAM_VERNAME" | tr -d '.')"
echo "versionName=$UPSTREAM_VERNAME" > rustdesk-version.txt
echo "versionCode=$UPSTREAM_VERCODE" >> rustdesk-version.txt
shell: bash
- name: Publish RustDesk version file
uses: softprops/action-gh-release@v1
with:
prerelease: true
tag_name: "fdroid-version"
files: |
./rustdesk-version.txt

View File

@@ -1,84 +0,0 @@
name: Flutter Windows History Build
on: [workflow_dispatch]
env:
LLVM_VERSION: "10.0"
FLUTTER_VERSION: "3.16.9"
TAG_NAME: "tmp"
FLUTTER_RUST_BRIDGE_VERSION: "1.80.1"
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
VERSION: "1.2.4"
jobs:
build-for-history-windows:
name: ${{ matrix.job.date }}
runs-on: ${{ matrix.job.os }}
strategy:
fail-fast: false
matrix:
job:
- { target: x86_64-pc-windows-msvc, os: windows-2019, arch: x86_64, date: 2023-08-04, ref: 72c198a1e94cc1e0242fce88f92b3f3caedcd0c3 }
steps:
- name: Checkout source code
uses: actions/checkout@v4
with:
ref: ${{ matrix.job.ref }}
- name: Install LLVM and Clang
uses: KyleMayes/install-llvm-action@v1
with:
version: ${{ env.LLVM_VERSION }}
- name: Install flutter
uses: subosito/flutter-action@v2
with:
channel: "stable"
flutter-version: ${{ env.FLUTTER_VERSION }}
cache: true
- name: Install Rust toolchain
uses: actions-rs/toolchain@v1
with:
toolchain: stable
target: ${{ matrix.job.target }}
override: true
components: rustfmt
profile: minimal # minimal component installation (ie, no documentation)
- name: Install flutter rust bridge deps
run: |
cargo install flutter_rust_bridge_codegen --version ${{ env.FLUTTER_RUST_BRIDGE_VERSION }} --features "uuid"
Push-Location flutter ; flutter pub get ; Pop-Location
~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart
- name: Setup vcpkg with Github Actions binary cache
uses: lukka/run-vcpkg@v11
with:
vcpkgDirectory: C:\vcpkg
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
- name: Install vcpkg dependencies
run: |
$VCPKG_ROOT/vcpkg install --x-install-root="$VCPKG_ROOT/installed"
shell: bash
- name: Build rustdesk
run: python3 .\build.py --portable --hwcodec --flutter --feature IddDriver
- name: Build self-extracted executable
shell: bash
run: |
pushd ./libs/portable
python3 ./generate.py -f ../../flutter/build/windows/runner/Release/ -o . -e ../../flutter/build/windows/runner/Release/rustdesk.exe
popd
mkdir -p ./SignOutput
mv ./target/release/rustdesk-portable-packer.exe ./SignOutput/rustdesk-${{ matrix.job.date }}-${{ matrix.job.target }}.exe
- name: Publish Release
uses: softprops/action-gh-release@v1
with:
prerelease: true
tag_name: ${{ env.TAG_NAME }}
files: |
./SignOutput/rustdesk-*.exe

416
.github/workflows/playground.yml vendored Normal file
View File

@@ -0,0 +1,416 @@
name: playground
on:
#schedule:
# schedule build every night
# - cron: "0/6 * * * *"
workflow_dispatch:
env:
RUST_VERSION: "1.75" # https://github.com/rustdesk/rustdesk/discussions/7503
CARGO_NDK_VERSION: "3.1.2"
LLVM_VERSION: "15.0.6"
FLUTTER_VERSION: "3.22.2"
FLUTTER_RUST_BRIDGE_VERSION: "1.80.1"
# for arm64 linux because official Dart SDK does not work
FLUTTER_ELINUX_VERSION: "3.16.9"
TAG_NAME: "nightly"
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
# vcpkg version: 2024.06.15
VCPKG_COMMIT_ID: "f7423ee180c4b7f40d43402c2feb3859161ef625"
VERSION: "1.3.2"
NDK_VERSION: "r26d"
#signing keys env variable checks
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
MACOS_P12_BASE64: "${{ secrets.MACOS_P12_BASE64 }}"
# To make a custom build with your own servers set the below secret values
RS_PUB_KEY: "${{ secrets.RS_PUB_KEY }}"
RENDEZVOUS_SERVER: "${{ secrets.RENDEZVOUS_SERVER }}"
API_SERVER: "${{ secrets.API_SERVER }}"
UPLOAD_ARTIFACT: "${{ inputs.upload-artifact }}"
SIGN_BASE_URL: "${{ secrets.SIGN_BASE_URL }}"
jobs:
build-for-macOS:
name: ${{ matrix.job.target }}
runs-on: ${{ matrix.job.os }}
strategy:
fail-fast: false
matrix:
job:
- {
target: x86_64-apple-darwin,
os: macos-13, #macos-latest or macos-14 use M1 now, https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners/about-github-hosted-runners#:~:text=14%20GB-,macos%2Dlatest%20or%20macos%2D14,-The%20macos%2Dlatestlabel
extra-build-args: "",
arch: x86_64,
flutter: "3.13.9",
ref: "f6509e3fd6917aa976bad2fc684182601ebf2434",
bridge: "1.80.1",
date: "20231219"
}
- {
target: x86_64-apple-darwin,
os: macos-13, #macos-latest or macos-14 use M1 now, https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners/about-github-hosted-runners#:~:text=14%20GB-,macos%2Dlatest%20or%20macos%2D14,-The%20macos%2Dlatestlabel
extra-build-args: "",
arch: x86_64,
flutter: "3.10.6",
ref: "f6509e3fd6917aa976bad2fc684182601ebf2434",
bridge: "1.80.1",
date: "20231219"
}
- {
target: x86_64-apple-darwin,
os: macos-13, #macos-latest or macos-14 use M1 now, https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners/about-github-hosted-runners#:~:text=14%20GB-,macos%2Dlatest%20or%20macos%2D14,-The%20macos%2Dlatestlabel
extra-build-args: "",
arch: x86_64,
flutter: "3.10.6",
ref: "85ddfc0739f052cab0029c46b899b959ee94eeb8",
bridge: "1.80.1",
date: "20231119"
}
- {
target: x86_64-apple-darwin,
os: macos-13, #macos-latest or macos-14 use M1 now, https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners/about-github-hosted-runners#:~:text=14%20GB-,macos%2Dlatest%20or%20macos%2D14,-The%20macos%2Dlatestlabel
extra-build-args: "",
arch: x86_64,
flutter: "3.13.9",
ref: "85ddfc0739f052cab0029c46b899b959ee94eeb8",
bridge: "1.80.1",
date: "20231119"
}
steps:
- name: Export GitHub Actions cache environment variables
uses: actions/github-script@v6
with:
script: |
core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || '');
core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || '');
- name: Checkout source code
uses: actions/checkout@v3
with:
ref: ${{ matrix.job.ref }}
- name: Import the codesign cert
if: env.MACOS_P12_BASE64 != null
uses: apple-actions/import-codesign-certs@v1
with:
p12-file-base64: ${{ secrets.MACOS_P12_BASE64 }}
p12-password: ${{ secrets.MACOS_P12_PASSWORD }}
keychain: rustdesk
- name: Check sign and import sign key
if: env.MACOS_P12_BASE64 != null
run: |
security default-keychain -s rustdesk.keychain
security find-identity -v
- name: Import notarize key
if: env.MACOS_P12_BASE64 != null
uses: timheuer/base64-to-file@v1.2
with:
# https://gregoryszorc.com/docs/apple-codesign/stable/apple_codesign_rcodesign.html#notarizing-and-stapling
fileName: rustdesk.json
fileDir: ${{ github.workspace }}
encodedString: ${{ secrets.MACOS_NOTARIZE_JSON }}
- name: Install rcodesign tool
if: env.MACOS_P12_BASE64 != null
shell: bash
run: |
pushd /tmp
wget https://github.com/indygreg/apple-platform-rs/releases/download/apple-codesign%2F0.22.0/apple-codesign-0.22.0-macos-universal.tar.gz
tar -zxvf apple-codesign-0.22.0-macos-universal.tar.gz
mv apple-codesign-0.22.0-macos-universal/rcodesign /usr/local/bin
popd
- name: Install build runtime
run: |
brew install llvm create-dmg nasm cmake gcc wget ninja pkg-config
- name: Install flutter
uses: subosito/flutter-action@v2
with:
channel: "stable"
flutter-version: ${{ matrix.job.flutter }}
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@v1
with:
toolchain: ${{ env.RUST_VERSION }}
targets: ${{ matrix.job.target }}
components: "rustfmt"
- uses: Swatinem/rust-cache@v2
with:
prefix-key: ${{ matrix.job.os }}
- name: Install flutter rust bridge deps
shell: bash
run: |
sed -i '' 's/3.1.0/2.17.0/g' flutter/pubspec.yaml;
cargo install flutter_rust_bridge_codegen --version ${{ matrix.job.bridge }} --features "uuid"
# below works for mac to make buildable on 3.13.9
# pushd flutter/lib; find . -name "*.dart" | xargs -I{} sed -i '' 's/textScaler: TextScaler.linear(\(.*\)),/textScaleFactor: \1,/g' {}; popd;
pushd flutter && flutter pub get && popd
~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart --c-output ./flutter/macos/Runner/bridge_generated.h
- name: Setup vcpkg with Github Actions binary cache
uses: lukka/run-vcpkg@v11
with:
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
- name: Install vcpkg dependencies
run: |
$VCPKG_ROOT/vcpkg install --x-install-root="$VCPKG_ROOT/installed"
- name: Restore from cache and install vcpkg
uses: lukka/run-vcpkg@v7
if: false
with:
setupOnly: true
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
- name: Install vcpkg dependencies
if: false
run: |
$VCPKG_ROOT/vcpkg install libvpx libyuv opus aom
- name: Show version information (Rust, cargo, Clang)
shell: bash
run: |
clang --version || true
rustup -V
rustup toolchain list
rustup default
cargo -V
rustc -V
- name: Build rustdesk
run: |
./build.py --flutter ${{ matrix.job.extra-build-args }}
- name: create unsigned dmg
run: |
CREATE_DMG="$(command -v create-dmg)"
CREATE_DMG="$(readlink -f "$CREATE_DMG")"
sed -i -e 's/MAXIMUM_UNMOUNTING_ATTEMPTS=3/MAXIMUM_UNMOUNTING_ATTEMPTS=7/' "$CREATE_DMG"
create-dmg --icon "RustDesk.app" 200 190 --hide-extension "RustDesk.app" --window-size 800 400 --app-drop-link 600 185 rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.dmg ./flutter/build/macos/Build/Products/Release/RustDesk.app
- name: Codesign app and create signed dmg
if: env.MACOS_P12_BASE64 != null
run: |
# Patch create-dmg to give more attempts to unmount image
CREATE_DMG="$(command -v create-dmg)"
CREATE_DMG="$(readlink -f "$CREATE_DMG")"
sed -i -e 's/MAXIMUM_UNMOUNTING_ATTEMPTS=3/MAXIMUM_UNMOUNTING_ATTEMPTS=7/' "$CREATE_DMG"
# Unlock keychain
security default-keychain -s rustdesk.keychain
security unlock-keychain -p ${{ secrets.MACOS_P12_PASSWORD }} rustdesk.keychain
# start sign the rustdesk.app and dmg
rm -rf *.dmg || true
codesign --force --options runtime -s ${{ secrets.MACOS_CODESIGN_IDENTITY }} --deep --strict ./flutter/build/macos/Build/Products/Release/RustDesk.app -vvv
create-dmg --icon "RustDesk.app" 200 190 --hide-extension "RustDesk.app" --window-size 800 400 --app-drop-link 600 185 rustdesk-${{ env.VERSION }}.dmg ./flutter/build/macos/Build/Products/Release/RustDesk.app
codesign --force --options runtime -s ${{ secrets.MACOS_CODESIGN_IDENTITY }} --deep --strict rustdesk-${{ env.VERSION }}.dmg -vvv
# notarize the rustdesk-${{ env.VERSION }}.dmg
rcodesign notary-submit --api-key-path ${{ github.workspace }}/rustdesk.json --staple rustdesk-${{ env.VERSION }}.dmg
- name: Rename rustdesk
run: |
for name in rustdesk*??.dmg; do
mv "$name" "${name%%.dmg}-${{ matrix.job.arch }}-flutter${{ matrix.job.flutter }}-flutter${{ matrix.job.date }}.dmg"
done
- name: Publish DMG package
uses: softprops/action-gh-release@v1
with:
prerelease: true
tag_name: ${{ env.TAG_NAME }}
files: |
rustdesk*-${{ matrix.job.arch }}*.dmg
build-rustdesk-android:
if: false
name: build rustdesk android apk ${{ matrix.job.target }}
runs-on: ${{ matrix.job.os }}
strategy:
fail-fast: false
matrix:
job:
- {
arch: aarch64,
target: aarch64-linux-android,
os: ubuntu-20.04,
openssl-arch: android-arm64,
ref: master, # latest
}
steps:
- name: Checkout source code
uses: actions/checkout@v3
with:
ref: ${{ matrix.job.ref }}
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y \
clang \
cmake \
curl \
gcc-multilib \
git \
g++ \
g++-multilib \
libayatana-appindicator3-dev\
libasound2-dev \
libc6-dev \
libclang-10-dev \
libgstreamer1.0-dev \
libgstreamer-plugins-base1.0-dev \
libgtk-3-dev \
libpam0g-dev \
libpulse-dev \
libva-dev \
libvdpau-dev \
libxcb-randr0-dev \
libxcb-shape0-dev \
libxcb-xfixes0-dev \
libxdo-dev \
libxfixes-dev \
llvm-10-dev \
nasm \
yasm \
ninja-build \
openjdk-11-jdk-headless \
pkg-config \
tree \
wget
- name: Install flutter
uses: subosito/flutter-action@v2
with:
channel: "stable"
flutter-version: ${{ env.FLUTTER_VERSION }}
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@v1
with:
toolchain: ${{ env.RUST_VERSION }}
components: "rustfmt"
- name: Install flutter rust bridge deps
run: |
git config --global core.longpaths true
cargo install flutter_rust_bridge_codegen --version ${{ env.FLUTTER_RUST_BRIDGE_VERSION }} --features "uuid"
sed -i 's/uni_links_desktop/#uni_links_desktop/g' flutter/pubspec.yaml
pushd flutter/lib; find . | grep dart | xargs sed -i 's/textScaler: TextScaler.linear(\(.*\)),/textScaleFactor: \1,/g'; popd;
pushd flutter ; flutter pub get ; popd
~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart
- uses: nttld/setup-ndk@v1
id: setup-ndk
with:
ndk-version: ${{ env.NDK_VERSION }}
add-to-path: true
- name: Setup vcpkg with Github Actions binary cache
uses: lukka/run-vcpkg@v11
with:
vcpkgDirectory: /opt/artifacts/vcpkg
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
- name: Install vcpkg dependencies
run: |
case ${{ matrix.job.target }} in
aarch64-linux-android)
./flutter/build_android_deps.sh arm64-v8a
;;
armv7-linux-androideabi)
./flutter/build_android_deps.sh armeabi-v7a
;;
esac
shell: bash
- name: Clone deps
shell: bash
run: |
pushd /opt
git clone https://github.com/rustdesk-org/rustdesk_thirdparty_lib.git --depth=1
ls -ls /opt/artifacts/vcpkg/installed/arm64-android/lib/
# cp -rf /opt/rustdesk_thirdparty_lib/vcpkg/* /opt/artifacts/vcpkg/
ls -ls /opt/artifacts/vcpkg/installed/arm64-android/lib/
- name: Build rustdesk lib
env:
ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
ANDROID_NDK_ROOT: ${{ steps.setup-ndk.outputs.ndk-path }}
run: |
rustup target add ${{ matrix.job.target }}
cargo install cargo-ndk --version ${{ env.CARGO_NDK_VERSION }}
case ${{ matrix.job.target }} in
aarch64-linux-android)
./flutter/ndk_arm64.sh
mkdir -p ./flutter/android/app/src/main/jniLibs/arm64-v8a
cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/arm64-v8a/librustdesk.so
;;
armv7-linux-androideabi)
./flutter/ndk_arm.sh
mkdir -p ./flutter/android/app/src/main/jniLibs/armeabi-v7a
cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/armeabi-v7a/librustdesk.so
;;
esac
- name: Build rustdesk
shell: bash
env:
JAVA_HOME: /usr/lib/jvm/java-11-openjdk-amd64
run: |
export PATH=/usr/lib/jvm/java-11-openjdk-amd64/bin:$PATH
# temporary use debug sign config
sed -i "s/signingConfigs.release/signingConfigs.debug/g" ./flutter/android/app/build.gradle
case ${{ matrix.job.target }} in
aarch64-linux-android)
mkdir -p ./flutter/android/app/src/main/jniLibs/arm64-v8a
cp ${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/aarch64-linux-android/libc++_shared.so ./flutter/android/app/src/main/jniLibs/arm64-v8a/
cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/arm64-v8a/librustdesk.so
# build flutter
pushd flutter
flutter build apk --release --target-platform android-arm64 --split-per-abi
mv build/app/outputs/flutter-apk/app-arm64-v8a-release.apk ../rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.apk
;;
armv7-linux-androideabi)
mkdir -p ./flutter/android/app/src/main/jniLibs/armeabi-v7a
cp ${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/arm-linux-androideabi/libc++_shared.so ./flutter/android/app/src/main/jniLibs/armeabi-v7a/
cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/armeabi-v7a/librustdesk.so
# build flutter
pushd flutter
flutter build apk --release --target-platform android-arm --split-per-abi
mv build/app/outputs/flutter-apk/app-armeabi-v7a-release.apk ../rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.apk
;;
esac
popd
mkdir -p signed-apk; pushd signed-apk
mv ../rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.apk ./rustdesk-test-${{ matrix.job.ref }}-${{ matrix.job.ndk }}.apk
- uses: r0adkll/sign-android-release@v1
name: Sign app APK
if: env.ANDROID_SIGNING_KEY != null
id: sign-rustdesk
with:
releaseDirectory: ./signed-apk
signingKeyBase64: ${{ secrets.ANDROID_SIGNING_KEY }}
alias: ${{ secrets.ANDROID_ALIAS }}
keyStorePassword: ${{ secrets.ANDROID_KEY_STORE_PASSWORD }}
keyPassword: ${{ secrets.ANDROID_KEY_PASSWORD }}
env:
# override default build-tools version (29.0.3) -- optional
BUILD_TOOLS_VERSION: "30.0.2"
- name: Publish signed apk package
uses: softprops/action-gh-release@v1
with:
prerelease: true
tag_name: ${{ env.TAG_NAME }}
files: |
${{steps.sign-rustdesk.outputs.signedReleaseFile}}

View File

@@ -0,0 +1,60 @@
name: build RustDeskTempTopMostWindow
on:
workflow_call:
inputs:
upload-artifact:
type: boolean
default: true
target:
description: 'Target'
required: true
type: string
default: 'windows-2022'
configuration:
description: 'Configuration'
required: true
type: string
default: 'Release'
platform:
description: 'Platform'
required: true
type: string
default: 'x64'
target_version:
description: 'TargetVersion'
required: true
type: string
default: 'Windows10'
env:
project_path: WindowInjection/WindowInjection.vcxproj
jobs:
build-RustDeskTempTopMostWindow:
runs-on: ${{ inputs.target }}
strategy:
fail-fast: false
env:
build_output_dir: RustDeskTempTopMostWindow/WindowInjection/${{ inputs.platform }}/${{ inputs.configuration }}
steps:
- name: Add MSBuild to PATH
uses: microsoft/setup-msbuild@v2
- name: Download the source code
run: |
git clone https://github.com/rustdesk-org/RustDeskTempTopMostWindow RustDeskTempTopMostWindow
# Build. commit 53b548a5398624f7149a382000397993542ad796 is tag v0.3
- name: Build the project
run: |
cd RustDeskTempTopMostWindow && git checkout 53b548a5398624f7149a382000397993542ad796
msbuild ${{ env.project_path }} -p:Configuration=${{ inputs.configuration }} -p:Platform=${{ inputs.platform }} /p:TargetVersion=${{ inputs.target_version }}
- name: Archive build artifacts
uses: actions/upload-artifact@master
if: ${{ inputs.upload-artifact }}
with:
name: topmostwindow-artifacts
path: |
./${{ env.build_output_dir }}/WindowInjection.dll

View File

@@ -4,9 +4,9 @@ on:
types: [released] types: [released]
jobs: jobs:
publish: publish:
runs-on: windows-latest # action can only be run on windows runs-on: ubuntu-latest
steps: steps:
- uses: vedantmgoyal2009/winget-releaser@v1 - uses: vedantmgoyal9/winget-releaser@main
with: with:
identifier: RustDesk.RustDesk identifier: RustDesk.RustDesk
version: ${{ github.event.release.tag_name }} version: ${{ github.event.release.tag_name }}

3
.gitignore vendored
View File

@@ -52,3 +52,6 @@ lib/generated_bridge.dart
examples/**/target/ examples/**/target/
# === # ===
vcpkg_installed vcpkg_installed
flutter/lib/generated_plugin_registrant.dart
libsciter.dylib
flutter/web/

3185
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +1,10 @@
[package] [package]
name = "rustdesk" name = "rustdesk"
version = "1.2.4" version = "1.3.2"
authors = ["rustdesk <info@rustdesk.com>"] authors = ["rustdesk <info@rustdesk.com>"]
edition = "2021" edition = "2021"
build= "build.rs" build= "build.rs"
description = "A remote control software." description = "RustDesk Remote Desktop"
default-run = "rustdesk" default-run = "rustdesk"
rust-version = "1.75" rust-version = "1.75"
@@ -19,19 +19,14 @@ path = "src/naming.rs"
[features] [features]
inline = [] inline = []
cli = [] cli = []
flutter_texture_render = []
appimage = []
flatpak = []
use_samplerate = ["samplerate"] use_samplerate = ["samplerate"]
use_rubato = ["rubato"] use_rubato = ["rubato"]
use_dasp = ["dasp"] use_dasp = ["dasp"]
flutter = ["flutter_rust_bridge"] flutter = ["flutter_rust_bridge"]
default = ["use_dasp"] default = ["use_dasp"]
hwcodec = ["scrap/hwcodec"] hwcodec = ["scrap/hwcodec"]
gpucodec = ["scrap/gpucodec"] vram = ["scrap/vram"]
mediacodec = ["scrap/mediacodec"] mediacodec = ["scrap/mediacodec"]
linux_headless = ["pam" ]
virtual_display_driver = ["virtual_display"]
plugin_framework = [] 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 = [
@@ -45,7 +40,8 @@ unix-file-copy-paste = [
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
whoami = "1.4" async-trait = "0.1"
whoami = "1.5.0"
scrap = { path = "libs/scrap", features = ["wayland"] } scrap = { path = "libs/scrap", features = ["wayland"] }
hbb_common = { path = "libs/hbb_common" } hbb_common = { path = "libs/hbb_common" }
serde_derive = "1.0" serde_derive = "1.0"
@@ -57,7 +53,6 @@ lazy_static = "1.4"
sha2 = "0.10" sha2 = "0.10"
repng = "0.2" repng = "0.2"
parity-tokio-ipc = { git = "https://github.com/rustdesk-org/parity-tokio-ipc" } parity-tokio-ipc = { git = "https://github.com/rustdesk-org/parity-tokio-ipc" }
runas = "=1.0" # https://github.com/mitsuhiko/rust-runas/issues/13
magnum-opus = { git = "https://github.com/rustdesk-org/magnum-opus" } magnum-opus = { git = "https://github.com/rustdesk-org/magnum-opus" }
dasp = { version = "0.11", features = ["signal", "interpolate-linear", "interpolate"], optional = true } dasp = { version = "0.11", features = ["signal", "interpolate-linear", "interpolate"], optional = true }
rubato = { version = "0.12", optional = true } rubato = { version = "0.12", optional = true }
@@ -65,14 +60,13 @@ samplerate = { version = "0.2", optional = true }
uuid = { version = "1.3", features = ["v4"] } uuid = { version = "1.3", features = ["v4"] }
clap = "4.2" clap = "4.2"
rpassword = "7.2" rpassword = "7.2"
base64 = "0.21"
num_cpus = "1.15" num_cpus = "1.15"
bytes = { version = "1.4", features = ["serde"] } bytes = { version = "1.4", features = ["serde"] }
default-net = "0.14" default-net = "0.14"
wol-rs = "1.0" wol-rs = "1.0"
flutter_rust_bridge = { version = "=1.80", features = ["uuid"], optional = true} flutter_rust_bridge = { version = "=1.80", features = ["uuid"], optional = true}
errno = "0.3" errno = "0.3"
rdev = { git = "https://github.com/fufesou/rdev" } rdev = { git = "https://github.com/rustdesk-org/rdev" }
url = { version = "2.3", features = ["serde"] } url = { version = "2.3", features = ["serde"] }
crossbeam-queue = "0.3" crossbeam-queue = "0.3"
hex = "0.4" hex = "0.4"
@@ -95,18 +89,35 @@ sys-locale = "0.3"
enigo = { path = "libs/enigo", features = [ "with_serde" ] } enigo = { path = "libs/enigo", features = [ "with_serde" ] }
clipboard = { path = "libs/clipboard" } clipboard = { path = "libs/clipboard" }
ctrlc = "3.2" ctrlc = "3.2"
arboard = { version = "3.2", features = ["wayland-data-control"] } # arboard = { version = "3.4.0", features = ["wayland-data-control"] }
arboard = { git = "https://github.com/rustdesk-org/arboard", features = ["wayland-data-control"] }
clipboard-master = { git = "https://github.com/rustdesk-org/clipboard-master" }
system_shutdown = "4.0" system_shutdown = "4.0"
qrcode-generator = "4.1" qrcode-generator = "4.1"
[target.'cfg(target_os = "windows")'.dependencies] [target.'cfg(target_os = "windows")'.dependencies]
winapi = { version = "0.3", features = ["winuser", "wincrypt", "shellscalingapi", "pdh", "synchapi", "memoryapi", "shellapi"] } winapi = { version = "0.3", features = [
"winuser",
"wincrypt",
"shellscalingapi",
"pdh",
"synchapi",
"memoryapi",
"shellapi",
"devguid",
"setupapi",
"cguid",
"cfgmgr32",
"ioapiset",
] }
winreg = "0.11" winreg = "0.11"
windows-service = "0.6" windows-service = "0.6"
virtual_display = { path = "libs/virtual_display", optional = true } virtual_display = { path = "libs/virtual_display" }
impersonate_system = { git = "https://github.com/21pages/impersonate-system" } impersonate_system = { git = "https://github.com/rustdesk-org/impersonate-system" }
shared_memory = "0.12" shared_memory = "0.12"
tauri-winrt-notification = "0.1.2" tauri-winrt-notification = "0.1.2"
runas = "1.2"
[target.'cfg(target_os = "macos")'.dependencies] [target.'cfg(target_os = "macos")'.dependencies]
objc = "0.2" objc = "0.2"
@@ -127,44 +138,47 @@ image = "0.24"
keepawake = { git = "https://github.com/rustdesk-org/keepawake-rs" } keepawake = { git = "https://github.com/rustdesk-org/keepawake-rs" }
[target.'cfg(any(target_os = "windows", target_os = "linux"))'.dependencies] [target.'cfg(any(target_os = "windows", target_os = "linux"))'.dependencies]
wallpaper = { git = "https://github.com/21pages/wallpaper.rs" } wallpaper = { git = "https://github.com/rustdesk-org/wallpaper.rs" }
[target.'cfg(any(target_os = "macos", target_os = "windows"))'.dependencies] [target.'cfg(any(target_os = "macos", target_os = "windows"))'.dependencies]
# https://github.com/rustdesk/rustdesk-server-pro/issues/189, using native-tls for better tls support # https://github.com/rustdesk/rustdesk-server-pro/issues/189, using native-tls for better tls support
reqwest = { git = "https://github.com/rustdesk-org/reqwest", features = ["blocking", "json", "native-tls", "gzip"], default-features=false } reqwest = { git = "https://github.com/rustdesk-org/reqwest", features = ["blocking", "socks", "json", "native-tls", "gzip"], default-features=false }
[target.'cfg(not(any(target_os = "macos", target_os = "windows")))'.dependencies] [target.'cfg(not(any(target_os = "macos", target_os = "windows")))'.dependencies]
reqwest = { git = "https://github.com/rustdesk-org/reqwest", features = ["blocking", "json", "rustls-tls", "rustls-tls-native-roots", "gzip"], default-features=false } reqwest = { git = "https://github.com/rustdesk-org/reqwest", features = ["blocking", "socks", "json", "rustls-tls", "rustls-tls-native-roots", "gzip"], default-features=false }
[target.'cfg(target_os = "linux")'.dependencies] [target.'cfg(target_os = "linux")'.dependencies]
psimple = { package = "libpulse-simple-binding", version = "2.27" } psimple = { package = "libpulse-simple-binding", version = "2.27" }
pulse = { package = "libpulse-binding", version = "2.27" } pulse = { package = "libpulse-binding", version = "2.27" }
rust-pulsectl = { git = "https://github.com/open-trade/pulsectl" } rust-pulsectl = { git = "https://github.com/open-trade/pulsectl" }
async-process = "1.7" async-process = "1.7"
mouce = { git="https://github.com/fufesou/mouce.git" } evdev = { git="https://github.com/rustdesk-org/evdev" }
evdev = { git="https://github.com/fufesou/evdev" }
dbus = "0.9" dbus = "0.9"
dbus-crossroads = "0.5" dbus-crossroads = "0.5"
pam = { git="https://github.com/fufesou/pam", optional = true } pam = { git="https://github.com/rustdesk-org/pam" }
users = { version = "0.11" } users = { version = "0.11" }
x11-clipboard = {git="https://github.com/clslaid/x11-clipboard", branch = "feat/store-batch", optional = true} x11-clipboard = {git="https://github.com/clslaid/x11-clipboard", branch = "feat/store-batch", optional = true}
x11rb = {version = "0.12", features = ["all-extensions"], optional = true} x11rb = {version = "0.12", features = ["all-extensions"], optional = true}
percent-encoding = {version = "2.3", optional = true} percent-encoding = {version = "2.3", optional = true}
once_cell = {version = "1.18", optional = true} once_cell = {version = "1.18", optional = true}
nix = { version = "0.29", features = ["term", "process"]}
gtk = "0.18"
termios = "0.3"
[target.'cfg(target_os = "android")'.dependencies] [target.'cfg(target_os = "android")'.dependencies]
android_logger = "0.13" android_logger = "0.13"
jni = "0.21" jni = "0.21"
android-wakelock = { git = "https://github.com/21pages/android-wakelock" } 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"] members = ["libs/scrap", "libs/hbb_common", "libs/enigo", "libs/clipboard", "libs/virtual_display", "libs/virtual_display/dylib", "libs/portable"]
exclude = ["vdi/host", "examples/custom_plugin"] exclude = ["vdi/host", "examples/custom_plugin"]
[package.metadata.winres] [package.metadata.winres]
LegalCopyright = "Copyright © 2023 Purslane, Inc." LegalCopyright = "Copyright © 2024 Purslane Ltd. All rights reserved."
# this FileDescription overrides package.description ProductName = "RustDesk"
FileDescription = "RustDesk" FileDescription = "RustDesk Remote Desktop"
OriginalFilename = "rustdesk.exe"
[target.'cfg(target_os="windows")'.build-dependencies] [target.'cfg(target_os="windows")'.build-dependencies]
winres = "0.1" winres = "0.1"

View File

@@ -18,6 +18,7 @@ RUN apt update -y && \
libxcb-shape0-dev \ libxcb-shape0-dev \
libxcb-xfixes0-dev \ libxcb-xfixes0-dev \
libasound2-dev \ libasound2-dev \
libpam0g-dev \
libpulse-dev \ libpulse-dev \
make \ make \
cmake \ cmake \

View File

@@ -1,6 +1,6 @@
<p align="center"> <p align="center">
<img src="res/logo-header.svg" alt="RustDesk - Your remote desktop"><br> <img src="res/logo-header.svg" alt="RustDesk - Your remote desktop"><br>
<a href="#free-public-servers">Servers</a> • <a href="#public-servers">Servers</a> •
<a href="#raw-steps-to-build">Build</a> • <a href="#raw-steps-to-build">Build</a> •
<a href="#how-to-build-with-docker">Docker</a> • <a href="#how-to-build-with-docker">Docker</a> •
<a href="#file-structure">Structure</a> • <a href="#file-structure">Structure</a> •
@@ -13,8 +13,6 @@ Chat with us: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitt
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09) [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09)
[![Open Bounties](https://img.shields.io/endpoint?url=https%3A%2F%2Fconsole.algora.io%2Fapi%2Fshields%2Frustdesk%2Fbounties%3Fstatus%3Dopen)](https://console.algora.io/org/rustdesk/bounties?status=open)
Yet another remote desktop software, written in Rust. Works out of the box, no configuration required. You have full control of your data, with no concerns about security. You can use our rendezvous/relay server, [set up your own](https://rustdesk.com/server), or [write your own rendezvous/relay server](https://github.com/rustdesk/rustdesk-server-demo). Yet another remote desktop software, written in Rust. Works out of the box, no configuration required. You have full control of your data, with no concerns about security. You can use our rendezvous/relay server, [set up your own](https://rustdesk.com/server), or [write your own rendezvous/relay server](https://github.com/rustdesk/rustdesk-server-demo).
![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png) ![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png)
@@ -31,22 +29,6 @@ RustDesk welcomes contribution from everyone. See [CONTRIBUTING.md](docs/CONTRIB
alt="Get it on F-Droid" alt="Get it on F-Droid"
height="80">](https://f-droid.org/en/packages/com.carriez.flutter_hbb) height="80">](https://f-droid.org/en/packages/com.carriez.flutter_hbb)
## Free Public Servers
Below are the servers you are using for free, they may change over time. If you are not close to one of these, your network may be slow.
| Location | Vendor | Specification |
| --------- | ------------- | ------------------ |
| Germany | [Hetzner](https://www.hetzner.com) | 2 vCPU / 4 GB RAM |
| Ukraine (Kyiv) | [dc.volia](https://dc.volia.com) | 2 vCPU / 4 GB RAM |
## Dev Container
[![Open in Dev Containers](https://img.shields.io/static/v1?label=Dev%20Container&message=Open&color=blue&logo=visualstudiocode)](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/rustdesk/rustdesk)
If you already have VS Code and Docker installed, you can click the badge above to get started. Clicking will cause VS Code to automatically install the Dev Containers extension if needed, clone the source code into a container volume, and spin up a dev container for use.
Go through [DEVCONTAINER.md](docs/DEVCONTAINER.md) for more info.
## Dependencies ## Dependencies
Desktop versions use Flutter or Sciter (deprecated) for GUI, this tutorial is for Sciter only, since it is easier and more friendly to start. Check out our [CI](https://github.com/rustdesk/rustdesk/blob/master/.github/workflows/flutter-build.yml) for building Flutter version. Desktop versions use Flutter or Sciter (deprecated) for GUI, this tutorial is for Sciter only, since it is easier and more friendly to start. Check out our [CI](https://github.com/rustdesk/rustdesk/blob/master/.github/workflows/flutter-build.yml) for building Flutter version.
@@ -77,19 +59,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 libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libpam0g-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 sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel pam-devel
``` ```
### Fedora 28 (CentOS 8) ### 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 sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel gstreamer1-devel gstreamer1-plugins-base-devel pam-devel
``` ```
### Arch (Manjaro) ### Arch (Manjaro)
@@ -180,12 +162,16 @@ Please ensure that you are running these commands from the root of the RustDesk
- **[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/js)**: JavaScript for Flutter web client - **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: JavaScript for Flutter web client
## Snapshots ## Screenshots
![image](https://user-images.githubusercontent.com/71636191/113112362-ae4deb80-923b-11eb-957d-ff88daad4f06.png) ![Connection Manager](https://github.com/rustdesk/rustdesk/assets/28412477/db82d4e7-c4bc-4823-8e6f-6af7eadf7651)
![image](https://user-images.githubusercontent.com/71636191/113112619-f705a480-923b-11eb-911d-97e984ef52b6.png) ![Connected to a Windows PC](https://github.com/rustdesk/rustdesk/assets/28412477/9baa91e9-3362-4d06-aa1a-7518edcbd7ea)
![image](https://user-images.githubusercontent.com/71636191/113112857-3fbd5d80-923c-11eb-9836-768325faf906.png) ![File Transfer](https://github.com/rustdesk/rustdesk/assets/28412477/39511ad3-aa9a-4f8c-8947-1cce286a46ad)
![image](https://user-images.githubusercontent.com/71636191/135385039-38fdbd72-379a-422d-b97f-33df71fb1cec.png) ![TCP Tunneling](https://github.com/rustdesk/rustdesk/assets/28412477/78e8708f-e87e-4570-8373-1360033ea6c5)
## [Public Servers](#public-servers)
RustDesk is supported by a free EU server, graciously provided by [Codext GmbH](https://codext.link/rustdesk?utm_source=github)

View File

@@ -2,7 +2,7 @@
version: 1 version: 1
script: script:
- rm -rf ./AppDir || true - rm -rf ./AppDir || true
- bsdtar -zxvf ../rustdesk-1.2.4.deb - bsdtar -zxvf rustdesk.deb
- tar -xvf ./data.tar.xz - tar -xvf ./data.tar.xz
- mkdir ./AppDir - mkdir ./AppDir
- mv ./usr ./AppDir/usr - mv ./usr ./AppDir/usr
@@ -18,7 +18,7 @@ AppDir:
id: rustdesk id: rustdesk
name: rustdesk name: rustdesk
icon: rustdesk icon: rustdesk
version: 1.2.4 version: 1.3.2
exec: usr/lib/rustdesk/rustdesk exec: usr/lib/rustdesk/rustdesk
exec_args: $@ exec_args: $@
apt: apt:
@@ -26,18 +26,18 @@ AppDir:
- arm64 - arm64
allow_unauthenticated: true allow_unauthenticated: true
sources: sources:
- sourceline: deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ bionic main restricted universe multiverse - sourceline: deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ focal main restricted universe multiverse
key_url: 'http://keyserver.ubuntu.com/pks/lookup?op=get&search=0x3b4fe6acc0b21f32' key_url: 'http://keyserver.ubuntu.com/pks/lookup?op=get&search=0x3b4fe6acc0b21f32'
- sourceline: deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ bionic-updates main restricted universe multiverse - sourceline: deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ focal-updates main restricted universe multiverse
key_url: 'http://keyserver.ubuntu.com/pks/lookup?op=get&search=0x3b4fe6acc0b21f32' key_url: 'http://keyserver.ubuntu.com/pks/lookup?op=get&search=0x3b4fe6acc0b21f32'
- sourceline: deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ bionic-backports main restricted - sourceline: deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ focal-backports main restricted
universe multiverse universe multiverse
key_url: 'http://keyserver.ubuntu.com/pks/lookup?op=get&search=0x3b4fe6acc0b21f32' key_url: 'http://keyserver.ubuntu.com/pks/lookup?op=get&search=0x3b4fe6acc0b21f32'
- sourceline: deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ bionic-security main restricted - sourceline: deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ focal-security main restricted
universe multiverse universe multiverse
key_url: 'http://keyserver.ubuntu.com/pks/lookup?op=get&search=0x3b4fe6acc0b21f32' key_url: 'http://keyserver.ubuntu.com/pks/lookup?op=get&search=0x3b4fe6acc0b21f32'
include: include:
- libc6 - libc6:arm64
- libgtk-3-0 - libgtk-3-0
- libxcb-randr0 - libxcb-randr0
- libxdo3 - libxdo3
@@ -51,9 +51,15 @@ AppDir:
- libva-x11-2 - libva-x11-2
- libvdpau1 - libvdpau1
- libgstreamer-plugins-base1.0-0 - libgstreamer-plugins-base1.0-0
- gstreamer1.0-pipewire
- libwayland-client0
- libwayland-cursor0 - libwayland-cursor0
- libwayland-egl1 - libwayland-egl1
- libpulse0 - libpulse0
- packagekit-gtk3-module
- libcanberra-gtk3-module
- libpam0g
- libdrm2
exclude: exclude:
- humanity-icon-theme - humanity-icon-theme
- hicolor-icon-theme - hicolor-icon-theme
@@ -69,8 +75,11 @@ AppDir:
- usr/share/doc/*/TODO.* - usr/share/doc/*/TODO.*
runtime: runtime:
env: env:
GIO_MODULE_DIR: $APPDIR/usr/lib/x86_64-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
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/lib/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_SYSTEM_PATH: /lib64/gstreamer-1.0:/usr/lib/aarch64-linux-gnu/gstreamer-1.0:$APPDIR/usr/lib/aarch64-linux-gnu/gstreamer-1.0
test: test:
fedora-30: fedora-30:
image: appimagecrafters/tests-env:fedora-30 image: appimagecrafters/tests-env:fedora-30

View File

@@ -2,7 +2,7 @@
version: 1 version: 1
script: script:
- rm -rf ./AppDir || true - rm -rf ./AppDir || true
- bsdtar -zxvf ../rustdesk-1.2.4.deb - bsdtar -zxvf rustdesk.deb
- tar -xvf ./data.tar.xz - tar -xvf ./data.tar.xz
- mkdir ./AppDir - mkdir ./AppDir
- mv ./usr ./AppDir/usr - mv ./usr ./AppDir/usr
@@ -18,7 +18,7 @@ AppDir:
id: rustdesk id: rustdesk
name: rustdesk name: rustdesk
icon: rustdesk icon: rustdesk
version: 1.2.4 version: 1.3.2
exec: usr/lib/rustdesk/rustdesk exec: usr/lib/rustdesk/rustdesk
exec_args: $@ exec_args: $@
apt: apt:
@@ -26,19 +26,20 @@ AppDir:
- amd64 - amd64
allow_unauthenticated: true allow_unauthenticated: true
sources: sources:
- sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic main restricted - sourceline: deb http://archive.ubuntu.com/ubuntu/ focal main restricted
- sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic-updates main restricted - sourceline: deb http://archive.ubuntu.com/ubuntu/ focal-updates main restricted
- sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic universe - sourceline: deb http://archive.ubuntu.com/ubuntu/ focal universe
- sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic-updates universe - sourceline: deb http://archive.ubuntu.com/ubuntu/ focal-updates universe
- sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic multiverse - sourceline: deb http://archive.ubuntu.com/ubuntu/ focal multiverse
- sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic-updates multiverse - sourceline: deb http://archive.ubuntu.com/ubuntu/ focal-updates multiverse
- sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic-backports main restricted - sourceline: deb http://archive.ubuntu.com/ubuntu/ focal-backports main restricted
universe multiverse universe multiverse
- sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic-security main restricted - sourceline: deb http://archive.ubuntu.com/ubuntu/ focal-security main restricted
universe multiverse universe multiverse
- sourceline: deb http://ppa.launchpad.net/pipewire-debian/pipewire-upstream/ubuntu
bionic main
include: include:
# https://github.com/rustdesk/rustdesk/issues/9103
# Because of APPDIR_LIBRARY_PATH, this libc6 is not used, use LD_PRELOAD: $APPDIR/usr/lib/x86_64-linux-gnu/libc.so.6 may help, If you have time, please have a try.
# We modify APPDIR_LIBRARY_PATH to use system lib first because gst crashed if not doing so, but you can try to change it.
- libc6:amd64 - libc6:amd64
- libgtk-3-0 - libgtk-3-0
- libxcb-randr0 - libxcb-randr0
@@ -54,9 +55,14 @@ AppDir:
- libvdpau1 - libvdpau1
- libgstreamer-plugins-base1.0-0 - libgstreamer-plugins-base1.0-0
- gstreamer1.0-pipewire - gstreamer1.0-pipewire
- libwayland-client0
- libwayland-cursor0 - libwayland-cursor0
- libwayland-egl1 - libwayland-egl1
- libpulse0 - libpulse0
- packagekit-gtk3-module
- libcanberra-gtk3-module
- libpam0g
- libdrm2
exclude: exclude:
- humanity-icon-theme - humanity-icon-theme
- hicolor-icon-theme - hicolor-icon-theme
@@ -72,8 +78,11 @@ AppDir:
- usr/share/doc/*/TODO.* - usr/share/doc/*/TODO.*
runtime: runtime:
env: env:
GIO_MODULE_DIR: $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
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/lib/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_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
test: test:
fedora-30: fedora-30:
image: appimagecrafters/tests-env:fedora-30 image: appimagecrafters/tests-env:fedora-30

View File

@@ -25,17 +25,22 @@ flutter_build_dir_2 = f'flutter/{flutter_build_dir}'
skip_cargo = False skip_cargo = False
def get_arch() -> str: def get_deb_arch() -> str:
custom_arch = os.environ.get("ARCH") custom_arch = os.environ.get("DEB_ARCH")
if custom_arch is None: if custom_arch is None:
return "amd64" return "amd64"
return custom_arch return custom_arch
def get_deb_extra_depends() -> str:
custom_arch = os.environ.get("DEB_ARCH")
if custom_arch == "armhf": # for arm32v7 libsciter-gtk.so
return ", libatomic1"
return ""
def system2(cmd): def system2(cmd):
err = os.system(cmd) exit_code = os.system(cmd)
if err != 0: if exit_code != 0:
print(f"Error occurred when executing: {cmd}. Exiting.") sys.stderr.write(f"Error occurred when executing: `{cmd}`. Exiting.\n")
sys.exit(-1) sys.exit(-1)
@@ -48,15 +53,7 @@ def get_version():
def parse_rc_features(feature): def parse_rc_features(feature):
available_features = { available_features = {}
'PrivacyMode': {
'platform': ['windows'],
'zip_url': 'https://github.com/fufesou/RustDeskTempTopMostWindow/releases/download/v0.3'
'/TempTopMostWindow_x64.zip',
'checksum_url': 'https://github.com/fufesou/RustDeskTempTopMostWindow/releases/download/v0.3/checksum_md5',
'include': ['WindowInjection.dll'],
}
}
apply_features = {} apply_features = {}
if not feature: if not feature:
feature = [] feature = []
@@ -80,8 +77,9 @@ def parse_rc_features(feature):
return get_all_features() return get_all_features()
elif isinstance(feature, list): elif isinstance(feature, list):
if windows: if windows:
# force add PrivacyMode # download third party is deprecated, we use github ci instead.
feature.append('PrivacyMode') # feature.append('PrivacyMode')
pass
for feat in feature: for feat in feature:
if isinstance(feat, str) and feat.upper() == 'ALL': if isinstance(feat, str) and feat.upper() == 'ALL':
return get_all_features() return get_all_features()
@@ -106,7 +104,7 @@ def make_parser():
nargs='+', nargs='+',
default='', default='',
help='Integrate features, windows only.' help='Integrate features, windows only.'
'Available: PrivacyMode. Special value is "ALL" and empty "". Default is empty.') 'Available: [Not used for now]. Special value is "ALL" and empty "". Default is empty.')
parser.add_argument('--flutter', action='store_true', parser.add_argument('--flutter', action='store_true',
help='Build flutter package', default=False) help='Build flutter package', default=False)
parser.add_argument( parser.add_argument(
@@ -116,9 +114,9 @@ def make_parser():
'' if windows or osx else ', need libva-dev, libvdpau-dev.') '' if windows or osx else ', need libva-dev, libvdpau-dev.')
) )
parser.add_argument( parser.add_argument(
'--gpucodec', '--vram',
action='store_true', action='store_true',
help='Enable feature gpucodec, only available on windows now.' help='Enable feature vram, only available on windows now.'
) )
parser.add_argument( parser.add_argument(
'--portable', '--portable',
@@ -130,21 +128,17 @@ def make_parser():
action='store_true', action='store_true',
help='Build with unix file copy paste feature' help='Build with unix file copy paste feature'
) )
parser.add_argument(
'--flatpak',
action='store_true',
help='Build rustdesk libs with the flatpak feature enabled'
)
parser.add_argument(
'--appimage',
action='store_true',
help='Build rustdesk libs with the appimage feature enabled'
)
parser.add_argument( parser.add_argument(
'--skip-cargo', '--skip-cargo',
action='store_true', action='store_true',
help='Skip cargo build process, only flutter version + Linux supported currently' help='Skip cargo build process, only flutter version + Linux supported currently'
) )
if windows:
parser.add_argument(
'--skip-portable-pack',
action='store_true',
help='Skip packing, only flutter version + Windows supported'
)
parser.add_argument( parser.add_argument(
"--package", "--package",
type=str type=str
@@ -188,6 +182,9 @@ def generate_build_script_for_docker():
system2("bash /tmp/build.sh") system2("bash /tmp/build.sh")
# Downloading third party resources is deprecated.
# We can use this function in an offline build environment.
# Even in an online environment, we recommend building third-party resources yourself.
def download_extract_features(features, res_dir): def download_extract_features(features, res_dir):
import re import re
@@ -271,15 +268,10 @@ def get_features(args):
features = ['inline'] if not args.flutter else [] features = ['inline'] if not args.flutter else []
if args.hwcodec: if args.hwcodec:
features.append('hwcodec') features.append('hwcodec')
if args.gpucodec: if args.vram:
features.append('gpucodec') features.append('vram')
if args.flutter: if args.flutter:
features.append('flutter') features.append('flutter')
features.append('flutter_texture_render')
if args.flatpak:
features.append('flatpak')
if args.appimage:
features.append('appimage')
if args.unix_file_copy_paste: if args.unix_file_copy_paste:
features.append('unix-file-copy-paste') features.append('unix-file-copy-paste')
print("features:", features) print("features:", features)
@@ -291,14 +283,17 @@ def generate_control_file(version):
system2('/bin/rm -rf %s' % control_file_path) system2('/bin/rm -rf %s' % control_file_path)
content = """Package: rustdesk content = """Package: rustdesk
Section: net
Priority: optional
Version: %s 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-0, libxcb-randr0, libxdo3, libxfixes3, libxcb-shape0, libxcb-xfixes0, libasound2, libsystemd0, curl, libva-drm2, libva-x11-2, libvdpau1, libgstreamer-plugins-base1.0-0, libpam0g, libappindicator3-1, gstreamer1.0-pipewire Depends: libgtk-3-0, libxcb-randr0, libxdo3, libxfixes3, libxcb-shape0, libxcb-xfixes0, libasound2, libsystemd0, curl, libva-drm2, libva-x11-2, libvdpau1, libgstreamer-plugins-base1.0-0, libpam0g, gstreamer1.0-pipewire%s
Recommends: libayatana-appindicator3-1
Description: A remote control software. Description: A remote control software.
""" % (version, get_arch()) """ % (version, get_deb_arch(), get_deb_extra_depends())
file = open(control_file_path, "w") file = open(control_file_path, "w")
file.write(content) file.write(content)
file.close() file.close()
@@ -338,8 +333,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/com.rustdesk.RustDesk.policy tmpdeb/usr/share/polkit-1/actions/')
system2( system2(
'cp ../res/startwm.sh tmpdeb/etc/rustdesk/') 'cp ../res/startwm.sh tmpdeb/etc/rustdesk/')
system2( system2(
@@ -383,8 +376,6 @@ def build_deb_from_folder(version, binary_folder):
'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/com.rustdesk.RustDesk.policy tmpdeb/usr/share/polkit-1/actions/')
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")
@@ -410,9 +401,11 @@ def build_flutter_dmg(version, features):
"cp target/release/liblibrustdesk.dylib target/release/librustdesk.dylib") "cp target/release/liblibrustdesk.dylib target/release/librustdesk.dylib")
os.chdir('flutter') os.chdir('flutter')
system2('flutter build macos --release') system2('flutter build macos --release')
'''
system2( system2(
"create-dmg --volname \"RustDesk Installer\" --window-pos 200 120 --window-size 800 400 --icon-size 100 --app-drop-link 600 185 --icon RustDesk.app 200 190 --hide-extension RustDesk.app rustdesk.dmg ./build/macos/Build/Products/Release/RustDesk.app") "create-dmg --volname \"RustDesk Installer\" --window-pos 200 120 --window-size 800 400 --icon-size 100 --app-drop-link 600 185 --icon RustDesk.app 200 190 --hide-extension RustDesk.app rustdesk.dmg ./build/macos/Build/Products/Release/RustDesk.app")
os.rename("rustdesk.dmg", f"../rustdesk-{version}.dmg") os.rename("rustdesk.dmg", f"../rustdesk-{version}.dmg")
'''
os.chdir("..") os.chdir("..")
@@ -427,7 +420,7 @@ def build_flutter_arch_manjaro(version, features):
system2('HBB=`pwd`/.. FLUTTER=1 makepkg -f') system2('HBB=`pwd`/.. FLUTTER=1 makepkg -f')
def build_flutter_windows(version, features): def build_flutter_windows(version, features, skip_portable_pack):
if not skip_cargo: if not skip_cargo:
system2(f'cargo build --features {features} --lib --release') system2(f'cargo build --features {features} --lib --release')
if not os.path.exists("target/release/librustdesk.dll"): if not os.path.exists("target/release/librustdesk.dll"):
@@ -438,6 +431,8 @@ def build_flutter_windows(version, features):
os.chdir('..') os.chdir('..')
shutil.copy2('target/release/deps/dylib_virtual_display.dll', shutil.copy2('target/release/deps/dylib_virtual_display.dll',
flutter_build_dir_2) flutter_build_dir_2)
if skip_portable_pack:
return
os.chdir('libs/portable') os.chdir('libs/portable')
system2('pip3 install -r requirements.txt') system2('pip3 install -r requirements.txt')
system2( system2(
@@ -487,7 +482,7 @@ def main():
os.chdir('../../..') os.chdir('../../..')
if flutter: if flutter:
build_flutter_windows(version, features) build_flutter_windows(version, features, args.skip_portable_pack)
return return
system2('cargo build --release --features ' + features) system2('cargo build --release --features ' + features)
# system2('upx.exe target/release/rustdesk.exe') # system2('upx.exe target/release/rustdesk.exe')

View File

@@ -1,9 +1,11 @@
#[cfg(windows)] #[cfg(windows)]
fn build_windows() { fn build_windows() {
let file = "src/platform/windows.cc"; let file = "src/platform/windows.cc";
cc::Build::new().file(file).compile("windows"); let file2 = "src/platform/windows_delete_test_cert.cc";
cc::Build::new().file(file).file(file2).compile("windows");
println!("cargo:rustc-link-lib=WtsApi32"); println!("cargo:rustc-link-lib=WtsApi32");
println!("cargo:rerun-if-changed={}", file); println!("cargo:rerun-if-changed={}", file);
println!("cargo:rerun-if-changed={}", file2);
} }
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]

View File

@@ -0,0 +1,87 @@
# 贡献者公约行为准则
## 我们的承诺
身为社区成员、贡献者和领袖,我们承诺使社区参与者不受骚扰,无论其年龄、体型、可见或不可见的缺陷、族裔、性征、性别认同和表达、经验水平、教育程度、社会与经济地位、国籍、相貌、种族、种姓、肤色、宗教信仰、性倾向或性取向如何。
我们承诺以有助于建立开放、友善、多样化、包容、健康社区的方式行事和互动。
## 我们的标准
有助于为我们的社区创造积极环境的行为例子包括但不限于:
* 表现出对他人的同情和善意
* 尊重不同的主张、观点和感受
* 提出和大方接受建设性意见
* 承担责任并向受我们错误影响的人道歉
* 注重社区共同诉求,而非个人得失
不当行为例子包括:
* 使用情色化的语言或图像,及性引诱或挑逗
* 嘲弄、侮辱或诋毁性评论,以及人身或政治攻击
* 公开或私下的骚扰行为
* 未经他人明确许可,公布他人的私人信息,如物理或电子邮件地址
* 其他有理由认定为违反职业操守的不当行为
## 责任和权力
社区领袖有责任解释和落实我们所认可的行为准则,并妥善公正地对他们认为不当、威胁、冒犯或有害的任何行为采取纠正措施。
社区领导有权力和责任删除、编辑或拒绝或拒绝与本行为准则不相符的评论comment、提交commits、代码、维基wiki编辑、议题issues或其他贡献并在适当时机知采取措施的理由。
## 适用范围
本行为准则适用于所有社区场合,也适用于在公共场所代表社区时的个人。
代表社区的情形包括使用官方电子邮件地址、通过官方社交媒体帐户发帖或在线上或线下活动中担任指定代表。
## 监督
辱骂、骚扰或其他不可接受的行为可通过[info@rustdesk.com](mailto:info@rustdesk.com)向负责监督的社区领袖报告。 所有投诉都将得到及时和公平的审查和调查。
所有社区领袖都有义务尊重任何事件报告者的隐私和安全。
## 处理方针
社区领袖将遵循下列社区处理方针来明确他们所认定违反本行为准则的行为的处理方式:
### 1. 纠正
**社区影响**: 使用不恰当的语言或其他在社区中被认定为不符合职业道德或不受欢迎的行为。
**处理意见**: 由社区领袖发出非公开的书面警告,明确说明违规行为的性质,并解释举止如何不妥。或将要求公开道歉。
### 2. 警告
**社区影响**: 单个或一系列违规行为。
**处理意见**: 警告并对连续性行为进行处理。在指定时间内,不得与相关人员互动,包括主动与行为准则执行者互动。这包括避免在社区场所和外部渠道中的互动。违反这些条款可能会导致临时或永久封禁。
### 3. 临时封禁
**社区影响**: 严重违反社区准则,包括持续的不当行为。
**处理意见**: 在指定时间内,暂时禁止与社区进行任何形式的互动或公开交流。在此期间,不得与相关人员进行公开或私下互动,包括主动与行为准则执行者互动。违反这些条款可能会导致永久封禁。
### 4. 永久封禁
**社区影响**: 行为模式表现出违反社区准则,包括持续的不当行为、骚扰个人或攻击或贬低某个类别的个体。
**处理意见**: 永久禁止在社区内进行任何形式的公开互动。
## 参见
本行为准则改编自[参与者公约][homepage]2.0 版, 参见
[https://www.contributor-covenant.org/zh-cn/version/2/0/code_of_conduct.html][v2.0].
指导方针借鉴自[Mozilla纪检分级][Mozilla CoC].
有关本行为准则的常见问题的答案,参见 [https://www.contributor-covenant.org/faq][FAQ]。 其他语言翻译参见[https://www.contributor-covenant.org/translations][translations]。
[homepage]: https://www.contributor-covenant.org
[v2.0]: https://www.contributor-covenant.org/zh-cn/version/2/0/code_of_conduct.html
[Mozilla CoC]: https://github.com/mozilla/diversity
[FAQ]: https://www.contributor-covenant.org/faq
[translations]: https://www.contributor-covenant.org/translations

32
docs/CONTRIBUTING-ZH.md Normal file
View File

@@ -0,0 +1,32 @@
# 为RustDesk做贡献
Rust欢迎每一位贡献者如果您有意向为我们做出贡献请遵循以下指南
## 贡献方式
对 RustDesk 或其依赖项的贡献需要通过 GitHub 的 Pull Request (PR) 的形式提交。每个 PR 都会由核心贡献者(即有权限合并代码的人)进行审核,审核通过后代码会合并到主分支,或者您会收到需要修改的反馈。所有贡献者,包括核心贡献者,提交的代码都应遵循此流程。
如果您希望处理某个问题,请先在对应的 GitHub issue 下发表评论,声明您将处理该问题,以避免该问题被多位贡献者重复处理。
## PR 注意事项
- 从 master 分支创建一个新的分支并在提交PR之前如果需要将您的分支 变基(rebase) 到最新的 master 分支。如果您的分支无法顺利合并到 master 分支,您可能会被要求更新您的代码。
- 每次提交的改动应该尽可能少,并且要保证每次提交的代码都是正确的(即每个 commit 都应能成功编译并通过测试)。
- 每个提交都应附有开发者证书签名(http://developercertificate.org), 表明您(以及您的雇主,若适用)同意遵守项目[许可证条款](../LICENCE)。在使用 git 提交代码时,可以通过在 `git commit` 时使用 `-s` 选项加入签名
- 如果您的 PR 未被及时审核,或需要指定的人员进行审核,您可以通过在 PR 或评论中 @ 提到相关审核者,以及发送[电子邮件](mailto:info@rustdesk.com)的方式请求审核。
- 请为修复的 bug 或新增的功能添加相应的测试用例。
有关具体的 git 使用说明,请参考[GitHub workflow 101](https://github.com/servo/servo/wiki/GitHub-workflow).
## 行为准则
请遵守项目的[贡献者公约行为准则](./CODE_OF_CONDUCT-ZH.md)。
## 沟通渠道
RustDesk 的贡献者主要通过 [Discord](https://discord.gg/nDceKgxnkV) 进行交流。

View File

@@ -27,14 +27,6 @@
[**BINARY تنزيل**](https://github.com/rustdesk/rustdesk/releases) [**BINARY تنزيل**](https://github.com/rustdesk/rustdesk/releases)
## خوادم مفتوحة ومجانية
فيما يلي الخوادم التي تستخدمها مجانًا، وقد تتغير طوال الوقت. إذا لم تكن قريبًا من أحد هؤلاء، فقد تكون شبكتك بطيئة.
| الموقع | المورد | المواصفات |
| --------- | ------------- | ------------------ |
| Germany | Hetzner | 2 vCPU / 4GB RAM |
| Ukraine (Kyiv) | [dc.volia](https://dc.volia.com) | 2 vCPU / 4 GB RAM |
## التبعيات ## التبعيات
لواجهة المستخدم الرسومية [sciter](https://sciter.com/) نسخة سطح المكتب تستخدم لواجهة المستخدم الرسومية [sciter](https://sciter.com/) نسخة سطح المكتب تستخدم

View File

@@ -22,14 +22,6 @@ Projekt RustDesk vítá přiložení ruky k dílu od každého. Jak začít se d
[**STAHOVÁNÍ ZKOMPILOVANÝCH APLIKACÍ**](https://github.com/rustdesk/rustdesk/releases) [**STAHOVÁNÍ ZKOMPILOVANÝCH APLIKACÍ**](https://github.com/rustdesk/rustdesk/releases)
## Veřejné, zdarma službu nabízející servery
Níže jsou uvedeny servery zdarma k vašemu použití (údaje se mohou v čase měnit). Pokud se nenacházíte v oblastech světa poblíž nich, spojení může být pomalé.
| umístění | dodavatel | parametry |
| --------- | ------------- | ------------------ |
| Germany | Hetzner | 2 vCPU / 4GB RAM |
| Ukraine (Kyiv) | [dc.volia](https://dc.volia.com) | 2 vCPU / 4 GB RAM |
## Softwarové součásti, na kterých závisí ## Softwarové součásti, na kterých závisí
Varianta pro počítač používá pro grafické uživatelské rozhraní [sciter](https://sciter.com/) stáhněte si potřebnou knihovnu. Varianta pro počítač používá pro grafické uživatelské rozhraní [sciter](https://sciter.com/) stáhněte si potřebnou knihovnu.

View File

@@ -19,14 +19,6 @@ RustDesk hilser bidrag fra alle velkommen. Se [`docs/CONTRIBUTING.md`](docs/CONT
[**PROGRAM DOWNLOAD**](https://github.com/rustdesk/rustdesk/releases) [**PROGRAM DOWNLOAD**](https://github.com/rustdesk/rustdesk/releases)
## Gratis offentlige servere
Nedenfor er de servere, du bruger gratis, det kan ændre sig med tiden. Hvis du ikke er tæt på en af disse, kan dit netværk være langsomt.
| Beliggenhed | Udbyder | Specifikation |
| ---------- | ------------- | ------------------ |
| Germany | Hetzner | 2 vCPU / 4GB RAM |
## Afhængigheder ## Afhængigheder
Desktopversioner bruger [sciter](https://sciter.com/) eller Flutter til GUI, denne vejledning er kun for Sciter. Desktopversioner bruger [sciter](https://sciter.com/) eller Flutter til GUI, denne vejledning er kun for Sciter.

View File

@@ -29,22 +29,6 @@ RustDesk heißt jegliche Mitarbeit willkommen. Schauen Sie sich [CONTRIBUTING-DE
alt="Get it on F-Droid" alt="Get it on F-Droid"
height="80">](https://f-droid.org/en/packages/com.carriez.flutter_hbb) height="80">](https://f-droid.org/en/packages/com.carriez.flutter_hbb)
## Freie öffentliche Server
Nachfolgend sind die Server gelistet, die Sie kostenlos nutzen können. Es kann sein, dass sich diese Liste immer mal wieder ändert. Falls Sie nicht in der Nähe einer dieser Server sind, kann es sein, dass Ihre Verbindung langsam sein wird.
| Standort | Anbieter | Spezifikation |
| --------- | ------------- | ------------------ |
| Deutschland | [Hetzner](https://www.hetzner.com/de/) | 2 vCPU / 4 GB RAM |
| Ukraine (Kiew) | [dc.volia](https://dc.volia.com) | 2 vCPU / 4 GB RAM |
## Dev-Container
[![In Dev-Containern öffnen](https://img.shields.io/static/v1?label=Dev%20Container&message=Open&color=blue&logo=visualstudiocode)](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/rustdesk/rustdesk)
Wenn Sie VS Code und Docker bereits installiert haben, können Sie auf das Abzeichen oben klicken, um loszulegen. Wenn Sie darauf klicken, wird VS Code automatisch die Dev-Container-Erweiterung installieren, den Quellcode in ein Container-Volume klonen und einen Dev-Container für die Verwendung aufsetzen.
Weitere Informationen finden Sie in [DEVCONTAINER-DE.md](DEVCONTAINER-DE.md).
## Abhängigkeiten ## Abhängigkeiten
Desktop-Versionen verwenden [Sciter](https://sciter.com/) oder Flutter für die GUI, dieses Tutorial ist nur für Sciter. Desktop-Versionen verwenden [Sciter](https://sciter.com/) oder Flutter für die GUI, dieses Tutorial ist nur für Sciter.

View File

@@ -19,14 +19,6 @@ RustDesk bonvenigas kontribuon de ĉiuj. Vidu [`docs/CONTRIBUTING.md`](CONTRIBUT
[**BINARA ELŜUTO**](https://github.com/rustdesk/rustdesk/releases) [**BINARA ELŜUTO**](https://github.com/rustdesk/rustdesk/releases)
## Senpagaj publikaj serviloj
Malsupre estas la serviloj, kiuj vi uzas senpage, ĝi povas ŝanĝi laŭlonge de la tempo. Se vi ne estas proksima de unu de tiuj, via reto povas esti malrapida.
| Situo | Vendanto | Detaloj |
| --------- | ------------- | ------------------ |
| Germany | Hetzner | 2 vCPU / 4GB RAM |
| Ukraine (Kyiv) | [dc.volia](https://dc.volia.com) | 2 vCPU / 4GB RAM |
## Dependantaĵoj ## Dependantaĵoj
La labortabla versio uzas [sciter](https://sciter.com/) por la interfaco, bonvolu elŝuti la bibliotekon dinamikan sciter. La labortabla versio uzas [sciter](https://sciter.com/) por la interfaco, bonvolu elŝuti la bibliotekon dinamikan sciter.

View File

@@ -25,15 +25,6 @@ RustDesk agradece la contribución de todo el mundo. Lee [`docs/CONTRIBUTING.md`
alt="Get it on F-Droid" alt="Get it on F-Droid"
height="80">](https://f-droid.org/en/packages/com.carriez.flutter_hbb) height="80">](https://f-droid.org/en/packages/com.carriez.flutter_hbb)
## Servidores gratis de uso público
A continuación se muestran los servidores gratuitos, pueden cambiar a medida que pasa el tiempo. Si no estás cerca de uno de ellos, tu conexión puede ser lenta.
| Ubicación | Compañía | Especificación |
| --------- | ------------- | ------------------ |
| Germany | Hetzner | 2 vCPU / 4GB RAM |
| Ukraine (Kyiv) | [dc.volia](https://dc.volia.com) | 2 vCPU / 4GB RAM |
## Dependencias ## Dependencias
La versión Desktop usa [Sciter](https://sciter.com/) o Flutter para el GUI, este tutorial es solo para Sciter. La versión Desktop usa [Sciter](https://sciter.com/) o Flutter para el GUI, este tutorial es solo para Sciter.

View File

@@ -25,13 +25,6 @@
[دریافت نرم‌افزار](https://github.com/rustdesk/rustdesk/releases) [دریافت نرم‌افزار](https://github.com/rustdesk/rustdesk/releases)
## سرورهای عمومی رایگان
شما مي‌توانید از سرورهای زیر به رایگان استفاده کنید. این لیست ممکن است به مرور زمان تغییر می‌کند. اگر به این سرورها نزدیک نیستید، ممکن است اتصال شما کند باشد.
| موقعیت | سرویس دهنده | مشخصات |
| --------- | ------------- | ------------------ |
| آلمان | Hetzner | 2 vCPU / 4GB RAM |
## وابستگی ها ## وابستگی ها
نسخه‌های رومیزی از [sciter](https://sciter.com/) برای رابط کاربری گرافیکی استفاده می‌کنند. خواهشمندیم کتابخانه‌ی پویای sciter را خودتان دانلود کنید از این منابع دریافت کنید. نسخه‌های رومیزی از [sciter](https://sciter.com/) برای رابط کاربری گرافیکی استفاده می‌کنند. خواهشمندیم کتابخانه‌ی پویای sciter را خودتان دانلود کنید از این منابع دریافت کنید.

View File

@@ -19,14 +19,6 @@ RustDesk toivottaa avustukset tervetulleiksi kaikilta. Katso lisätietoja [`docs
[**BINAARILATAUS**](https://github.com/rustdesk/rustdesk/releases) [**BINAARILATAUS**](https://github.com/rustdesk/rustdesk/releases)
## Vapaita julkisia palvelimia
Alla on palvelimia, joita voit käyttää ilmaiseksi, ne saattavat muuttua ajan mittaan. Jos et ole lähellä yhtä näistä, verkkosi voi olla hidas.
| Sijainti | Myyjä | Määrittely |
| --------- | ------------- | ------------------ |
| Germany | Hetzner | 2 vCPU / 4GB RAM |
| Ukraine (Kyiv) | [dc.volia](https://dc.volia.com) | 2 vCPU / 4GB RAM |
## Riippuvuudet ## Riippuvuudet
Desktop-versiot käyttävät [sciter](https://sciter.com/) graafisena käyttöliittymänä, lataa sciter-dynaaminen kirjasto itsellesi. Desktop-versiot käyttävät [sciter](https://sciter.com/) graafisena käyttöliittymänä, lataa sciter-dynaaminen kirjasto itsellesi.

View File

@@ -19,14 +19,6 @@ RustDesk accueille les contributions de tout le monde. Voir [`docs/CONTRIBUTING.
[**TÉLÉCHARGEMENT BINAIRE**](https://github.com/rustdesk/rustdesk/releases) [**TÉLÉCHARGEMENT BINAIRE**](https://github.com/rustdesk/rustdesk/releases)
## Serveurs publics libres
Ci-dessous se trouvent les serveurs que vous utilisez gratuitement, cela peut changer au fil du temps. Si vous n'êtes pas proche de l'un d'entre eux, votre réseau peut être lent.
| Location | Vendor | Specification |
| --------- | ------------- | ------------------ |
| Germany | Hetzner | 2 vCPU / 4GB RAM |
## Dépendances ## Dépendances
Les versions de bureau utilisent [sciter](https://sciter.com/) pour l'interface graphique, veuillez télécharger la bibliothèque dynamique sciter vous-même. Les versions de bureau utilisent [sciter](https://sciter.com/) pour l'interface graphique, veuillez télécharger la bibliothèque dynamique sciter vous-même.

View File

@@ -29,22 +29,6 @@
alt="Get it on F-Droid" alt="Get it on F-Droid"
height="80">](https://f-droid.org/en/packages/com.carriez.flutter_hbb) height="80">](https://f-droid.org/en/packages/com.carriez.flutter_hbb)
## Δωρεάν δημόσιοι διακομιστές
Παρακάτω είναι οι διακομιστές που χρησιμοποιούνται δωρεάν, ενδέχεται να αλλάξουν με την πάροδο του χρόνου. Εάν δεν είστε κοντά σε ένα από αυτούς, το δίκτυό σας ίσως να είναι αργό.
| Περιοχή | Πάροχος | Προδιαγραφές |
| --------- | ------------- | ------------------ |
| Γερμανία | Hetzner | 2 vCPU / 4GB RAM |
| Ουκρανία (Κίεβο) | [dc.volia](https://dc.volia.com) | 2 vCPU / 4GB RAM |
## Dev Container
[![Open in Dev Containers](https://img.shields.io/static/v1?label=Dev%20Container&message=Open&color=blue&logo=visualstudiocode)](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/rustdesk/rustdesk)
Αν έχετε εγκατεστημένα το VS Code και το Docker, μπορείτε να ξεκινήσετε κάνοντας κλικ στην παραπάνω εικόνα. Αυτό θα έχει ως αποτέλεσμα, το VS Code να εγκαταστήσει αυτόματα την επέκταση Dev Containers, εάν χρειάζεται, θα κλωνοποιήσει τον πηγαίο κώδικα σε έναν νέο container και θα εκκινήσει ένα Dev Container για χρήση προγραμματισμού.
Για περισσότερες πληροφορίες μεταβείτε στο [DEVCONTAINER.md](docs/DEVCONTAINER.md).
## Προαπαιτούμενα για build ## Προαπαιτούμενα για build
Στις παραθυρικές εκδόσεις χρησιμοποιείται είτε το [sciter](https://sciter.com/) είτε το Flutter, τα παρακάτω βήματα είναι μόνο για το Sciter. Στις παραθυρικές εκδόσεις χρησιμοποιείται είτε το [sciter](https://sciter.com/) είτε το Flutter, τα παρακάτω βήματα είναι μόνο για το Sciter.

View File

@@ -27,14 +27,6 @@ A RustDesk szívesen fogad minden contributiont, támogatást mindenkitől. Lás
alt="Get it on F-Droid" alt="Get it on F-Droid"
height="80">](https://f-droid.org/en/packages/com.carriez.flutter_hbb) height="80">](https://f-droid.org/en/packages/com.carriez.flutter_hbb)
## Ingyenes publikus szerverek
Ezalatt az üzenet alatt találhatóak azok a publikus szerverek, amelyeket ingyen használhatsz. Ezek a szerverek változhatnak a jövőben, illetve a hálózatuk lehet hogy lassú lehet.
| Hely | Host | Specifikáció |
| --------- | ------------- | ------------------ |
| Germany | Hetzner | 2 vCPU / 4GB RAM |
| Ukraine (Kyiv) | [dc.volia](https://dc.volia.com) | 2 vCPU / 4GB RAM |
## Dependencies ## Dependencies
Az asztali verziók [sciter](https://sciter.com/)-t használnak a GUI-hoz, kérlek telepítsd a dynamikus könyvtárat magad. Az asztali verziók [sciter](https://sciter.com/)-t használnak a GUI-hoz, kérlek telepítsd a dynamikus könyvtárat magad.

View File

@@ -31,20 +31,6 @@ RustDesk mengajak semua orang untuk ikut berkontribusi. Lihat [`docs/CONTRIBUTIN
alt="Get it on F-Droid" alt="Get it on F-Droid"
height="80">](https://f-droid.org/en/packages/com.carriez.flutter_hbb) height="80">](https://f-droid.org/en/packages/com.carriez.flutter_hbb)
## Server Publik Gratis
Di bawah ini merupakan server gratis yang bisa kamu gunakan, seiring dengan waktu mungkin akan terjadi perubahan spesifikasi pada setiap server yang ada. Jika lokasi kamu berada jauh dengan salah satu server yang tersedia, kemungkinan koneksi akan terasa lambat ketika melakukan proses remote.
| Lokasi | Penyedia | Spesifikasi |
| --------- | ------------- | ------------------ |
| Jerman | [Hetzner](https://www.hetzner.com) | 2 vCPU / 4GB RAM |
| Ukraina (Kyiv) | [dc.volia](https://dc.volia.com) | 2 vCPU / 4GB RAM |
## Dev Container
[![Open in Dev Containers](https://img.shields.io/static/v1?label=Dev%20Container&message=Open&color=blue&logo=visualstudiocode)](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/rustdesk/rustdesk)
Apabila PC kamu sudah terinstal VS Code dan Docker, kamu bisa mengklik badge yang ada diatas untuk memulainya. Dengan mengklik badge tersebut secara otomatis akan menginstal ekstensi pada VS Code, lakukan kloning (clone) source code kedalam container volume, dan aktifkan dev container untuk menggunakannya.
## Dependensi ## Dependensi
Pada versi desktop, antarmuka pengguna (GUI) menggunakan [Sciter](https://sciter.com/) atau flutter Pada versi desktop, antarmuka pengguna (GUI) menggunakan [Sciter](https://sciter.com/) atau flutter

View File

@@ -4,8 +4,8 @@
<a href="#passaggi-per-la-compilazione">Compilazione</a> • <a href="#passaggi-per-la-compilazione">Compilazione</a> •
<a href="#come-compilare-con-docker">Docker</a> • <a href="#come-compilare-con-docker">Docker</a> •
<a href="#struttura-dei-file">Struttura</a> • <a href="#struttura-dei-file">Struttura</a> •
<a href="#screenshots">Schermate</a><br> <a href="#schermate">Schermate</a><br>
[<a href="../README.md">English</a>] | [<a href="README-UA.md">Українська</a>] | [<a href="README-CS.md">česky</a>] | [<a href="README-ZH.md">中文</a>] | [<a href="README-HU.md">Magyar</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-VN.md">Tiếng Việt</a>] | [<a href="README-GR.md">Ελληνικά</a>]<br> [<a href="../README.md">English</a>] | [<a href="README-UA.md">Українська</a>] | [<a href="README-CS.md">česky</a>] | [<a href="README-ZH.md">中文</a>] | [<a href="README-HU.md">Magyar</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-VN.md">Tiếng Việt</a>] | [<a href="README-DA.md">Dansk</a>] | [<a href="README-GR.md">Ελληνικά</a>] | [<a href="README-TR.md">Türkçe</a>]<br>
<b>Abbiamo bisogno del tuo aiuto per tradurre questo file README e la <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">UI RustDesk</a> nella tua lingua nativa</b> <b>Abbiamo bisogno del tuo aiuto per tradurre questo file README e la <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">UI RustDesk</a> nella tua lingua nativa</b>
</p> </p>
@@ -13,28 +13,29 @@ Chatta con noi su: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09) [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09)
Ancora un altro software per il controllo remoto del desktop, scritto in Rust. [![Bounties aperti](https://img.shields.io/endpoint?url=https%3A%2F%2Fconsole.algora.io%2Fapi%2Fshields%2Frustdesk%2Fbounties%3Fstatus%3Dopen)](https://console.algora.io/org/rustdesk/bounties?status=open)
Funziona immediatamente, nessuna configurazione richiesta. Hai il pieno controllo dei tuoi dati, senza preoccupazioni per la sicurezza.
Puoi usare il nostro server rendezvous/relay, [configurare il tuo server](https://rustdesk.com/server) o [realizzare il tuo server rendezvous/relay](https://github.com/rustdesk/rustdesk-server-demo).
RustDesk accoglie il contributo di tutti. Ancora un altro software per il controllo remoto del desktop, scritto in Rust. Funziona immediatamente, nessuna configurazione richiesta. Hai il pieno controllo dei tuoi dati, senza preoccupazioni per la sicurezza. Puoi usare il nostro server rendezvous/relay, [configurare il tuo server](https://rustdesk.com/server) o [realizzare il tuo server rendezvous/relay](https://github.com/rustdesk/rustdesk-server-demo).
Per ulteriori informazioni su come iniziare a contribuire, vedi [`docs/CONTRIBUTING-IT.md`](CONTRIBUTING.md).
[**DOWNLOAD PROGRAMMA**](https://github.com/rustdesk/rustdesk/releases) ![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png)
## Server pubblici gratuiti RustDesk accoglie il contributo di tutti. Per ulteriori informazioni su come iniziare a contribuire, vedi [CONTRIBUTING.md](CONTRIBUTING-IT.md).
Qui sotto trovi i server che possono essere usati gratuitamente, la lista potrebbe cambiare nel tempo. [**FAQ**](https://github.com/rustdesk/rustdesk/wiki/FAQ)
Se non sei vicino a uno di questi server, la connessione potrebbe essere lenta.
| Posizione | Venditore | Specifiche | [**SCARICA PROGRAMMA**](https://github.com/rustdesk/rustdesk/releases)
| --------- | ------------- | ------------------ |
| Germania | Hetzner | 2 vCPU / 4GB RAM | [**SCARICA NIGHTLY**](https://github.com/rustdesk/rustdesk/releases/tag/nightly)
| Ucraina (Kyev) | [dc.volia](https://dc.volia.com) | 2 vCPU / 4GB RAM |
[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png"
alt="Get it on F-Droid"
height="80">](https://f-droid.org/en/packages/com.carriez.flutter_hbb)
## Dipendenze ## Dipendenze
La versione Desktop usa per la GUI [sciter](https://sciter.com/), per favore scarica la libreria dinamica sciter. Le versioni desktop utilizzano Flutter o Sciter (deprecato) per l'interfaccia utente, questo tutorial è solo per Sciter, poiché è più facile per iniziare. Controlla il nostro [CI](https://github.com/rustdesk/rustdesk/blob/master/.github/workflows/flutter-build.yml) per la compilazione della versione Flutter.
Scarica la libreria dinamica Sciter.
[Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) | [Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) |
[Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) | [Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) |
@@ -51,12 +52,22 @@ La versione Desktop usa per la GUI [sciter](https://sciter.com/), per favore sca
- Esegui `cargo run` - Esegui `cargo run`
## [Build](https://rustdesk.com/docs/en/dev/build/)
## Come compilare in Linux ## Come compilare in Linux
### Ubuntu 18 (Debian 10) ### Ubuntu 18 (Debian 10)
```sh ```sh
sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake sudo apt install -y zip g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev \
libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake make \
libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev
```
### openSUSE Tumbleweed
```sh
sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel
``` ```
### Fedora 28 (CentOS 8) ### Fedora 28 (CentOS 8)
@@ -125,10 +136,7 @@ Quindi, ogni volta che devi compilare l'applicazione, esegui il seguente comando
docker run --rm -it -v $PWD:/home/user/rustdesk -v rustdesk-git-cache:/home/user/.cargo/git -v rustdesk-registry-cache:/home/user/.cargo/registry -e PUID="$(id -u)" -e PGID="$(id -g)" rustdesk-builder docker run --rm -it -v $PWD:/home/user/rustdesk -v rustdesk-git-cache:/home/user/.cargo/git -v rustdesk-registry-cache:/home/user/.cargo/registry -e PUID="$(id -u)" -e PGID="$(id -g)" rustdesk-builder
``` ```
Tieni presente che la prima build potrebbe richiedere più tempo prima che le dipendenze vengano memorizzate nella cache, le build successive saranno più veloci. Tieni presente che la prima build potrebbe richiedere più tempo prima che le dipendenze vengano memorizzate nella cache, le build successive saranno più veloci. Inoltre, se hai bisogno di specificare argomenti diversi per il comando build, puoi farlo alla fine del comando nella posizione `<OPTIONAL-ARGS>`. Ad esempio, se vuoi creare una versione di rilascio ottimizzata, esegui il comando precedentemente indicato seguito da `--release`. L'eseguibile generato sarà creato nella cartella destinazione del sistema e può essere eseguito con:
Inoltre, se hai bisogno di specificare argomenti diversi per il comando build, puoi farlo alla fine del comando nella posizione `<OPTIONAL-ARGS>`.
Ad esempio, se vuoi creare una versione di rilascio ottimizzata, esegui il comando precedentemente indicato seguito da `--release`.
L'eseguibile generato sarà creato nella cartella destinazione del sistema e può essere eseguito con:
```sh ```sh
target/debug/rustdesk target/debug/rustdesk
@@ -140,19 +148,21 @@ Oppure, se stai avviando un eseguibile di rilascio:
target/release/rustdesk target/release/rustdesk
``` ```
Assicurati di eseguire questi comandi dalla radice del repository RustDesk, altrimenti l'applicazione potrebbe non essere in grado di trovare le risorse richieste. Assicurati di eseguire questi comandi dalla radice del repository RustDesk, altrimenti l'applicazione potrebbe non essere in grado di trovare le risorse richieste. Nota inoltre che altri sottocomandi cargo come `install` o `run` non sono attualmente supportati tramite questo metodo poiché installerebbero o eseguirebbero il programma all'interno del container anziché nell'host.
Nota inoltre che altri sottocomandi cargo come `install` o `run` non sono attualmente supportati tramite questo metodo poiché installerebbero o eseguirebbero il programma all'interno del container anziché nell'host.
## Struttura dei file ## Struttura dei file
- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: codec video, config, wrapper tcp/udp, protobuf, funzioni per il trasferimento file, e altre funzioni utili. - **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: codec video, config, wrapper tcp/udp, protobuf, funzioni per il trasferimento file, e altre funzioni utili.
- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: cattura dello schermo - **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: cattura dello schermo
- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: controllo tastiera/mouse specifico della piattaforma - **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: controllo tastiera/mouse specifico della piattaforma
- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: GUI - **[libs/clipboard](https://github.com/rustdesk/rustdesk/tree/master/libs/clipboard)**: implementazione del copia e incolla dei file per Windows, Linux, macOS.
- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: Sciter UI obsoleto (deprecato)
- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: servizi audio/appunti/input/video e connessioni di rete - **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: servizi audio/appunti/input/video e connessioni di rete
- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: avvio di una connessione peer - **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: avvio di una connessione peer
- **[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/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: JavaScript per client web Flutter
## Schermate ## Schermate

View File

@@ -1,67 +1,73 @@
<p align="center"> <p align="center">
<img src="../res/logo-header.svg" alt="RustDesk - Your remote desktop"><br> <img src="../res/logo-header.svg" alt="RustDesk - あなたのためのリモートデスクトップ"><br>
<a href="#free-public-servers">Servers</a> • <a href="#free-public-servers">Servers</a> •
<a href="#raw-steps-to-build">Build</a> • <a href="#raw-steps-to-build">Build</a> •
<a href="#how-to-build-with-docker">Docker</a> • <a href="#how-to-build-with-docker">Docker</a> •
<a href="#file-structure">Structure</a> • <a href="#file-structure">Structure</a> •
<a href="#snapshot">Snapshot</a><br> <a href="#snapshot">Snapshot</a><br>
[<a href="../README.md">English</a>] | [<a href="README-UA.md">Українська</a>] | [<a href="README-CS.md">česky</a>] | [<a href="README-ZH.md">中文</a>] | [<a href="README-HU.md">Magyar</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-VN.md">Tiếng Việt</a>] | [<a href="README-GR.md">Ελληνικά</a>]<br> [<a href="docs/README-UA.md">Українська</a>] | [<a href="docs/README-CS.md">česky</a>] | [<a href="docs/README-ZH.md">中文</a>] | [<a href="docs/README-HU.md">Magyar</a>] | [<a href="docs/README-ES.md">Español</a>] | [<a href="docs/README-FA.md">فارسی</a>] | [<a href="docs/README-FR.md">Français</a>] | [<a href="docs/README-DE.md">Deutsch</a>] | [<a href="docs/README-PL.md">Polski</a>] | [<a href="docs/README-ID.md">Indonesian</a>] | [<a href="docs/README-FI.md">Suomi</a>] | [<a href="docs/README-ML.md">മലയാളം</a>] | [<a href="docs/README-JP.md">日本語</a>] | [<a href="docs/README-NL.md">Nederlands</a>] | [<a href="docs/README-IT.md">Italiano</a>] | [<a href="docs/README-RU.md">Русский</a>] | [<a href="docs/README-PTBR.md">Português (Brasil)</a>] | [<a href="docs/README-EO.md">Esperanto</a>] | [<a href="docs/README-KR.md">한국어</a>] | [<a href="docs/README-AR.md">العربي</a>] | [<a href="docs/README-VN.md">Tiếng Việt</a>] | [<a href="docs/README-DA.md">Dansk</a>] | [<a href="docs/README-GR.md">Ελληνικά</a>] | [<a href="docs/README-TR.md">Türkçe</a>]<br>
<b>このREADMEをあなたの母国語に翻訳するために、あなたの助けが必要です。</b> <b>READMEや<a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">RustDesk UI</a>、 <a href="https://github.com/rustdesk/doc.rustdesk.com">RustDesk Doc</a>の翻訳者を歓迎します!</b>
</p> </p>
Chat with us: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) 私たちと話す: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09) [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09)
Rustで書かれた、設定不要ですぐに使えるリモートデスクトップソフトウェアです。自分のデータを完全にコントロールでき、セキュリティの心配もありません。私たちのランデブー/リレーサーバを使うことも、[自分で設定する](https://rustdesk.com/server) ことも、 [自分でランデブー/リレーサーバを書くこともできます](https://github.com/rustdesk/rustdesk-server-demo)。 Rustで書かれた、設定不要ですぐに使えるリモートデスクトップソフトウェアです。自分のデータを完全にコントロールでき、セキュリティの心配もありません。私たちのランデブー/リレーサーバを使うことも、[自分でサーバーをセットアップする](https://rustdesk.com/server) ことも、 [自分でランデブー/リレーサーバを作成する](https://github.com/rustdesk/rustdesk-server-demo)こともできます
![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png) ![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png)
RustDeskは誰からの貢献歓迎します。 貢献するには [`docs/CONTRIBUTING.md`](CONTRIBUTING.md) を参照してください。 RustDeskは皆さんの貢献歓迎します。
貢献の方法については[CONTRIBUTING.md](docs/CONTRIBUTING.md)をご確認ください。
[**RustDeskはどの様に動くのか?**](https://github.com/rustdesk/rustdesk/wiki/How-does-RustDesk-work%3F) [**よくある質問**](https://github.com/rustdesk/rustdesk/wiki/FAQ)
[**BINARY DOWNLOAD**](https://github.com/rustdesk/rustdesk/releases) [**パッケージのダウンロード**](https://github.com/rustdesk/rustdesk/releases)
## 無料のパブリックサーバー [**ナイトリービルド**](https://github.com/rustdesk/rustdesk/releases/tag/nightly)
下記のサーバーは、無料で使用できますが、後々変更されることがあります。これらのサーバーから遠い場合、接続が遅い可能性があります。 [<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png"
| Location | Vendor | Specification | alt="F-Droidで入手する"
| --------- | ------------- | ------------------ | height="80">](https://f-droid.org/en/packages/com.carriez.flutter_hbb)
| Germany | Hetzner | 2 vCPU / 4GB RAM |
## 依存関係 ## 依存関係
デスクトップ版ではGUIに [sciter](https://sciter.com/) が使われています。 sciter dynamic library をダウンロードしてください。 デスクトップ版ではGUIにFlutterまたはSciter(非推奨)を使用しますが、チュートリアルでは分かりやすく、簡単なSciterのみを対象に解説しています。Flutterでのビルド方法については[CI](https://github.com/rustdesk/rustdesk/blob/master/.github/workflows/flutter-build.yml)をご覧ください。
Sciter dynamic libraryを事前にダウンロードしてください。
[Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) | [Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) |
[Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) | [Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) |
[MacOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib) [macOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib)
モバイル版はFlutterを利用します。デスクトップ版もSciterからFlutterへマイグレーション予定です。
## ビルド手順 ## ビルド手順
- Rust開発環境とC ++ビルド環境を準備します - Rust開発環境とC++ビルド環境を準備します
- [vcpkg](https://github.com/microsoft/vcpkg), をインストールし、 `VCPKG_ROOT` 環境変数を正しく設定します。 - [vcpkg](https://github.com/microsoft/vcpkg)をインストールし、環境変数に`VCPKG_ROOT`設定します。
その後、以下のコマンドを実行します。
- Windows: vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static aom:x64-windows-static
- Linux/MacOS: vcpkg install libvpx libyuv opus aom
- run `cargo run`
- Windowsの場合: vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static aom:x64-windows-static
- Linux/macOSの場合: vcpkg install libvpx libyuv opus aom
- `cargo run`を実行します。
## [ビルド](https://rustdesk.com/docs/en/dev/build/) ## [ビルド](https://rustdesk.com/docs/en/dev/build/)
## Linuxでのビルド手順 ## Linuxでのビルド方法
### Ubuntu 18 (Debian 10) ### Ubuntu 18 (Debian 10)
```sh ```sh
sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake sudo apt install -y zip g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev \
libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake make \
libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev
```
### openSUSE Tumbleweed
```sh
sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel
``` ```
### Fedora 28 (CentOS 8) ### Fedora 28 (CentOS 8)
@@ -76,7 +82,7 @@ sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-
sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pipewire sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pipewire
``` ```
### Install vcpkg ### vcpkgのインストール
```sh ```sh
git clone https://github.com/microsoft/vcpkg git clone https://github.com/microsoft/vcpkg
@@ -88,7 +94,7 @@ export VCPKG_ROOT=$HOME/vcpkg
vcpkg/vcpkg install libvpx libyuv opus aom vcpkg/vcpkg install libvpx libyuv opus aom
``` ```
### Fix libvpx (For Fedora) ### libvpxの修正 (Fedoraのみ)
```sh ```sh
cd vcpkg/buildtrees/libvpx/src cd vcpkg/buildtrees/libvpx/src
@@ -114,9 +120,9 @@ mv libsciter-gtk.so target/debug
VCPKG_ROOT=$HOME/vcpkg cargo run VCPKG_ROOT=$HOME/vcpkg cargo run
``` ```
## Dockerでビルドする方法 ## Dockerでビルド方法
リポジトリクローンを作成し、Dockerコンテナを構築することから始めます リポジトリクローンし、Dockerコンテナを構築ます:
```sh ```sh
git clone https://github.com/rustdesk/rustdesk git clone https://github.com/rustdesk/rustdesk
@@ -124,44 +130,50 @@ cd rustdesk
docker build -t "rustdesk-builder" . docker build -t "rustdesk-builder" .
``` ```
その後、アプリケーションをビルドする必要があるたびに、以下のコマンドを実行します 以下のコマンドを実行します:
```sh ```sh
docker run --rm -it -v $PWD:/home/user/rustdesk -v rustdesk-git-cache:/home/user/.cargo/git -v rustdesk-registry-cache:/home/user/.cargo/registry -e PUID="$(id -u)" -e PGID="$(id -g)" rustdesk-builder docker run --rm -it -v $PWD:/home/user/rustdesk -v rustdesk-git-cache:/home/user/.cargo/git -v rustdesk-registry-cache:/home/user/.cargo/registry -e PUID="$(id -u)" -e PGID="$(id -g)" rustdesk-builder
``` ```
このコマンドはRustDeskをビルドする度に実行する必要があります。
なお、最初のビルドでは、依存関係がキャッシュされるまで時間がかかることがありますが、その後のビルドではより速くなります。さらに、ビルドコマンドに別の引数を指定する必要がある場合は、コマンドの最後にある `<OPTIONAL-ARGS>` の位置で指定することができます。例えば、最適化されたリリースバージョンをビルドしたい場合は、上記のコマンドの後に 初回ビルドは時間がかかるかもしれませんが、2回目以降は依存関係がキャッシュされるため、ビルドにかかる時間が短くなります。
`--release` を実行します。できあがった実行ファイルはシステムのターゲットフォルダに格納され、のコマンドで実行できます。 ビルドコマンドに追加の引数を指定する必要がある場合は、コマンドの最後(`<OPTIONAL-ARGS>`の位置)で指定することができます。例えば、最適化されたリリースバージョンをビルドしたい場合は、上記のコマンドの後に `--release`追記し実行します。ビルドされた実行ファイルはあなたのシステムのターゲットフォルダに保存され、下記のコマンドで実行することができます。
デバッグビルドを起動する場合:
```sh ```sh
target/debug/rustdesk target/debug/rustdesk
``` ```
あるいは、リリース用の実行ファイルを実行している場合: リリースビルドを起動する場合:
```sh ```sh
target/release/rustdesk target/release/rustdesk
``` ```
これらのコマンドをRustDeskリポジトリのルートから実行していることを確認してください。そうしないと、アプリケーションが必要なリソースを見つけられない可能性があります。また、 `install``run` などの他の cargo サブコマンドは、ホストではなくコンテナ内プログラムをインストールまたは実行するため、現在の方法ではサポートされていないことに注意してください コマンドをRustDeskリポジトリのルートから実行していることを確認してください。また、`install``run` などの他のcargoサブコマンドは、ホストではなくコンテナ内プログラムをインストール実行するため、現在の方法ではサポートされていません
## ファイル構造 ## ファイル構造
- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: ビデオコーデック、コンフィグ、tcp/udpラッパー、protobuf、ファイル転送用のfs関数その他のユーティリティ関数 - **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: ビデオコーデック、設定、tcp/udpラッパー、protobuf、ファイル転送に利用されるfs関数その他のユーティリティ関数
- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: スクリーンキャプチャ - **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: スクリーンキャプチャ
- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: プラットフォーム固有のキーボード/マウスコントロール - **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: プラットフォーム固有のキーボード/マウス操作
- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: GUI - **[libs/clipboard](https://github.com/rustdesk/rustdesk/tree/master/libs/clipboard)**: Windows、Linux、macOS向けのファイルのコピーと貼り付けの実装
- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: オーディオ/クリップボード/入力/ビデオサービス、ネットワーク接続 - **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: 廃止された Sciter UI (非推奨)
- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**:
オーディオ/クリップボード/入力/ビデオ サービスとネットワーク接続
- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: ピア接続の開始 - **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: ピア接続の開始
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: [rustdesk-server](https://github.com/rustdesk/rustdesk-server), と通信し、リモートダイレクト (TCP hole punching) または中継接続を待つ - **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: [rustdesk-server](https://github.com/rustdesk/rustdesk-server)と通信し、リモートの直接接続(TCPホールパンチング)や中継接続を担う
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: プラットフォーム固有のコード - **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: プラットフォーム固有のコード
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: デスクトップとモバイル向けのFlutterコード
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: Flutterウェブクライアント向けのJavaScript
## スナップショット ## スクリーンショット
![image](https://user-images.githubusercontent.com/71636191/113112362-ae4deb80-923b-11eb-957d-ff88daad4f06.png) ![Connection Manager](https://github.com/rustdesk/rustdesk/assets/28412477/db82d4e7-c4bc-4823-8e6f-6af7eadf7651)
![image](https://user-images.githubusercontent.com/71636191/113112619-f705a480-923b-11eb-911d-97e984ef52b6.png) ![Connected to a Windows PC](https://github.com/rustdesk/rustdesk/assets/28412477/9baa91e9-3362-4d06-aa1a-7518edcbd7ea)
![image](https://user-images.githubusercontent.com/71636191/113112857-3fbd5d80-923c-11eb-9836-768325faf906.png) ![File Transfer](https://github.com/rustdesk/rustdesk/assets/28412477/39511ad3-aa9a-4f8c-8947-1cce286a46ad)
![image](https://user-images.githubusercontent.com/71636191/135385039-38fdbd72-379a-422d-b97f-33df71fb1cec.png) ![TCP Tunneling](https://github.com/rustdesk/rustdesk/assets/28412477/78e8708f-e87e-4570-8373-1360033ea6c5)

View File

@@ -24,13 +24,6 @@ RustDesk는 모든 기여를 환영합니다. 기여하고자 한다면 [`docs/C
[**BINARY DOWNLOAD**](https://github.com/rustdesk/rustdesk/releases) [**BINARY DOWNLOAD**](https://github.com/rustdesk/rustdesk/releases)
## 무료 퍼블릭 서버
표에 있는 서버는 무료로 사용할 수 있지만 추후 변경될 수도 있습니다. 이 서버에서 멀다면, 네트워크가 느려질 가능성도 있습니다.
| Location | Vendor | Specification |
| --------- | ------------- | ------------------ |
| Germany | Hetzner | 2 vCPU / 4GB RAM |
## 의존관계 ## 의존관계
데스크탑판에는 GUI에 [sciter](https://sciter.com/)가 사용되었습니다. sciter dynamic library 를 다운로드해주세요. 데스크탑판에는 GUI에 [sciter](https://sciter.com/)가 사용되었습니다. sciter dynamic library 를 다운로드해주세요.

View File

@@ -19,13 +19,6 @@
[**BINARY DOWNLOAD**](https://github.com/rustdesk/rustdesk/releases) [**BINARY DOWNLOAD**](https://github.com/rustdesk/rustdesk/releases)
## സൗജന്യ പൊതു സെർവറുകൾ
നിങ്ങൾ സൗജന്യമായി ഉപയോഗിക്കുന്ന സെർവറുകൾ ചുവടെയുണ്ട്, അത് സമയത്തിനനുസരിച്ച് മാറിയേക്കാം. നിങ്ങൾ ഇവയിലൊന്നിനോട് അടുത്തല്ലെങ്കിൽ, നിങ്ങളുടെ നെറ്റ്‌വർക്ക് സ്ലോ ആയേക്കാം.
| സ്ഥാനം | കച്ചവടക്കാരൻ | വിവരണം |
| --------- | ------------- | ------------------ |
| Germany | Hetzner | 2 vCPU / 4GB RAM |
## ഡിപെൻഡൻസികൾ ## ഡിപെൻഡൻസികൾ
ഡെസ്‌ക്‌ടോപ്പ് പതിപ്പുകൾ GUI-യ്‌ക്കായി [sciter](https://sciter.com/) ഉപയോഗിക്കുന്നു, ദയവായി സ്‌സൈറ്റർ ഡൈനാമിക് ലൈബ്രറി സ്വയം ഡൗൺലോഡ് ചെയ്യുക. ഡെസ്‌ക്‌ടോപ്പ് പതിപ്പുകൾ GUI-യ്‌ക്കായി [sciter](https://sciter.com/) ഉപയോഗിക്കുന്നു, ദയവായി സ്‌സൈറ്റർ ഡൈനാമിക് ലൈബ്രറി സ്വയം ഡൗൺലോഡ് ചെയ്യുക.

View File

@@ -27,22 +27,6 @@ RustDesk verwelkomt bijdragen van iedereen. Zie [`docs/CONTRIBUTING.md`](CONTRIB
alt="Download het op F-Droid" alt="Download het op F-Droid"
height="80">](https://f-droid.org/en/packages/com.carriez.flutter_hbb) height="80">](https://f-droid.org/en/packages/com.carriez.flutter_hbb)
## Gratis openbare servers
Hieronder staan de servers die u gratis gebruikt, ze kunnen in de loop van de tijd veranderen. Als u niet in de buurt van een van deze servers bevindt, kan uw vervinding langzamer zijn.
| Locatie | Aanbieder | Specificaties |
| --------- | ------------- | ------------------ |
| Duitsland | Hetzner | 2 vCPU / 4GB RAM |
| Oekraine (Kyiv) | [dc.volia](https://dc.volia.com) | 2 vCPU / 4GB RAM |
## Dev Container
[![Open in Dev Containers](https://img.shields.io/static/v1?label=Dev%20Container&message=Open&color=blue&logo=visualstudiocode)](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/rustdesk/rustdesk)
Als u VS Code en Docker al hebt geinstalleerd, kunt u op de bovenstaande badge klikken om te beginnen. Door te klikken zal VS Code automatisch de Dev Containers-extensie installeren indien nodig, de broncode klonen naar een containervolume en een dev container opstarten voor gebruik.
Bekijk [DEVCONTAINER.md](docs/DEVCONTAINER.md) voor meer informatie.
## Afhankelijkheden ## Afhankelijkheden
Desktop versies gebruiken [sciter](https://sciter.com/) of Flutter voor GUI, deze handleiding is alleen voor Sciter. Desktop versies gebruiken [sciter](https://sciter.com/) of Flutter voor GUI, deze handleiding is alleen voor Sciter.

View File

@@ -29,22 +29,6 @@ RustDesk zaprasza do współpracy każdego. Zobacz [`docs/CONTRIBUTING-PL.md`](C
alt="Get it on F-Droid" alt="Get it on F-Droid"
height="80">](https://f-droid.org/en/packages/com.carriez.flutter_hbb) height="80">](https://f-droid.org/en/packages/com.carriez.flutter_hbb)
## Darmowe Serwery Publiczne
Poniżej znajdują się serwery, z których można korzystać za darmo, może się to zmienić z upływem czasu. Jeśli nie znajdujesz się w pobliżu jednego z nich, Twoja prędkość połączenia może być niska.
| Lokalizacja | Dostawca | Specyfikacja |
| --------- | ------------- | ------------------ |
| Niemcy | Hetzner | 2 vCPU / 4GB RAM |
| Ukraina (Kijów) | [dc.volia](https://dc.volia.com) | 2 vCPU / 4GB RAM |
## Konterner Programisty (Dev Container)
[![Otwórz w Kontenerze programisty](https://img.shields.io/static/v1?label=Dev%20Container&message=Open&color=blue&logo=visualstudiocode)](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/rustdesk/rustdesk)
Jeżeli masz zainstalowany VS Code i Docker, możesz kliknąć w powyższy link, aby rozpocząć. Kliknięcie spowoduje automatyczną instalację rozszrzenia Kontenera Programisty w VS Code (jeżeli wymagany), sklonuje kod źródłowy do kontenera, i przygotuje kontener do użycia.
Więcej informacji w pliku [DEVCONTAINER-PL.md](docs/DEVCONTAINER-PL.md) for more info.
## Zależności ## Zależności
Wersje desktopowe używają [sciter](https://sciter.com/) dla GUI, proszę pobrać samodzielnie bibliotekę sciter. Wersje desktopowe używają [sciter](https://sciter.com/) dla GUI, proszę pobrać samodzielnie bibliotekę sciter.

View File

@@ -19,14 +19,6 @@ RustDesk acolhe contribuições de todos. Leia [`docs/CONTRIBUTING.md`](CONTRIBU
[**DOWNLOAD DE BINÁRIOS**](https://github.com/rustdesk/rustdesk/releases) [**DOWNLOAD DE BINÁRIOS**](https://github.com/rustdesk/rustdesk/releases)
## Servidores Públicos Grátis
Abaixo estão os servidores que você está utilizando de graça, ele pode mudar com o tempo. Se você não está próximo de algum deles, sua conexão pode ser lenta.
| Localização | Fornecedor | Especificações |
| ----------- | ------------- | ------------------ |
| Germany | Hetzner | 2 vCPU / 4GB RAM |
## Dependências ## Dependências
Versões de desktop utilizam [sciter](https://sciter.com/) para a GUI, por favor baixe a biblioteca dinâmica sciter por conta própria. Versões de desktop utilizam [sciter](https://sciter.com/) para a GUI, por favor baixe a biblioteca dinâmica sciter por conta própria.

View File

@@ -28,13 +28,6 @@ RustDesk приветствует вклад каждого. Ознакомьт
[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png" alt="Get it on F-Droid" height="80">](https://f-droid.org/en/packages/com.carriez.flutter_hbb) [<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png" alt="Get it on F-Droid" height="80">](https://f-droid.org/en/packages/com.carriez.flutter_hbb)
## Бесплатные общедоступные серверы
Ниже приведены бесплатные публичные сервера, используемые по умолчанию. Имейте ввиду, они могут меняться со временем. Также стоит отметить, что скорость работы сети зависит от вашего местоположения и расстояния до серверов. Подключение происходит к ближайшему доступному.
| Расположение | Поставщик | Технические характеристики |
| --------- | ------------- | ------------------ |
| Германия | Hetzner | 2 vCPU / 4GB RAM |
## Зависимости ## Зависимости
Настольные версии используют [sciter](https://sciter.com/) для графического интерфейса, загрузите динамическую библиотеку sciter самостоятельно. Настольные версии используют [sciter](https://sciter.com/) для графического интерфейса, загрузите динамическую библиотеку sciter самостоятельно.

View File

@@ -30,23 +30,6 @@ RustDesk, herkesten katkıyı kabul eder. Başlamak için [CONTRIBUTING.md](docs
alt="F-Droid'de Alın" alt="F-Droid'de Alın"
height="80">](https://f-droid.org/en/packages/com.carriez.flutter_hbb) height="80">](https://f-droid.org/en/packages/com.carriez.flutter_hbb)
## Ücretsiz Genel Sunucular
Aşağıda ücretsiz olarak kullandığınız sunucular listelenmiştir, zaman içinde değişebilirler. Eğer bunlardan birine yakın değilseniz, ağınız yavaş olabilir.
| Konum | Sağlayıcı | Özellikler |
| --------- | ------------- | ------------------ |
| Almanya | [Hetzner](https://www.hetzner.com) | 2 vCPU / 4 GB RAM |
| Almanya | [Codext](https://codext.de) | 4 vCPU / 8 GB RAM |
| Ukrayna (Kiev) | [dc.volia](https://dc.volia.com) | 2 vCPU / 4 GB RAM |
## Geliştirici Konteyneri
[![Open in Dev Containers](https://img.shields.io/static/v1?label=Dev%20Container&message=Open&color=blue&logo=visualstudiocode)](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/rustdesk/rustdesk)
Eğer zaten VS Code ve Docker kurulu ise yukarıdaki rozete tıklayarak başlayabilirsiniz. Tıklamak, VS Code'un gerektiğinde Dev Konteyner eklentisini otomatik olarak yüklemesine, kaynak kodunu bir konteyner hacmine klonlamasına ve kullanım için bir geliştirici konteyneri başlatmasına neden olur.
Daha fazla bilgi için [DEVCONTAINER.md](docs/DEVCONTAINER-TR.md) belgesine bakabilirsiniz.
## Bağımlılıklar ## Bağımlılıklar
Masaüstü sürümleri GUI için Masaüstü sürümleri GUI için

View File

@@ -1,20 +1,18 @@
<p align="center"> <p align="center">
<img src="../res/logo-header.svg" alt="RustDesk - Ваша віддалена стільниця"><br> <img src="../res/logo-header.svg" alt="RustDesk - Ваша віддалена стільниця"><br>
<a href="#безкоштовні-загальнодоступні-сервери">Сервери</a> • <a href="#публічні-сервери">Сервери</a> •
<a href="#кроки-для-збірки">Збирання</a> • <a href="#кроки-для-збірки">Збирання</a> •
<a href="#як-зібрати-за-допомогою-docker">Docker</a> • <a href="#як-зібрати-за-допомогою-docker">Docker</a> •
<a href="#структура-файлів">Структура</a> • <a href="#структура-файлів">Структура</a> •
<a href="#знімки">Знімки</a><br> <a href="#знімки-екрана">Знімки екрана</a><br>
[<a href="../README.md">English</a>] | [<a href="docs/README-CS.md">česky</a>] | [<a href="docs/README-ZH.md">中文</a>] | [<a href="docs/README-HU.md">Magyar</a>] | [<a href="docs/README-ES.md">Español</a>] | [<a href="docs/README-FA.md">فارسی</a>] | [<a href="docs/README-FR.md">Français</a>] | [<a href="docs/README-DE.md">Deutsch</a>] | [<a href="docs/README-PL.md">Polski</a>] | [<a href="docs/README-ID.md">Indonesian</a>] | [<a href="docs/README-FI.md">Suomi</a>] | [<a href="docs/README-ML.md">മലയാളം</a>] | [<a href="docs/README-JP.md">日本語</a>] | [<a href="docs/README-NL.md">Nederlands</a>] | [<a href="docs/README-IT.md">Italiano</a>] | [<a href="docs/README-RU.md">Русский</a>] | [<a href="docs/README-PTBR.md">Português (Brasil)</a>] | [<a href="docs/README-EO.md">Esperanto</a>] | [<a href="docs/README-KR.md">한국어</a>] | [<a href="docs/README-AR.md">العربي</a>] | [<a href="docs/README-VN.md">Tiếng Việt</a>] | [<a href="docs/README-DA.md">Dansk</a>] | [<a href="docs/README-GR.md">Ελληνικά</a>] | [<a href="docs/README-TR.md">Türkçe</a>]<br> [<a href="../README.md">English</a>] | [<a href="README-CS.md">česky</a>] | [<a href="README-ZH.md">中文</a>] | [<a href="README-HU.md">Magyar</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-VN.md">Tiếng Việt</a>] | [<a href="README-DA.md">Dansk</a>] | [<a href="README-GR.md">Ελληνικά</a>] | [<a href="README-TR.md">Türkçe</a>]<br>
<b>Нам потрібна ваша допомога для перекладу цього README, <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">інтерфейсу</a> та <a href="https://github.com/rustdesk/doc.rustdesk.com">документації</a> RustDesk на вашу рідну мову</B> <b>Нам потрібна ваша допомога для перекладу цього README, <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">інтерфейсу</a> та <a href="https://github.com/rustdesk/doc.rustdesk.com">документації</a> RustDesk вашою рідною мовою</B>
</p> </p>
Спілкування з нами: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) Спілкування з нами: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09) [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09)
[![Open Bounties](https://img.shields.io/endpoint?url=https%3A%2F%2Fconsole.algora.io%2Fapi%2Fshields%2Frustdesk%2Fbounties%3Fstatus%3Dopen)](https://console.algora.io/org/rustdesk/bounties?status=open)
Ще один застосунок для віддаленого керування стільницею, написаний на Rust. Працює з коробки, не потребує налаштування. Ви повністю контролюєте свої дані, не турбуючись про безпеку. Ви можете використовувати наш сервер ретрансляції, [налаштувати свій власний](https://rustdesk.com/server), або [написати свій власний сервер ретрансляції](https://github.com/rustdesk/rustdesk-server-demo). Ще один застосунок для віддаленого керування стільницею, написаний на Rust. Працює з коробки, не потребує налаштування. Ви повністю контролюєте свої дані, не турбуючись про безпеку. Ви можете використовувати наш сервер ретрансляції, [налаштувати свій власний](https://rustdesk.com/server), або [написати свій власний сервер ретрансляції](https://github.com/rustdesk/rustdesk-server-demo).
![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png) ![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png)
@@ -31,22 +29,6 @@ RustDesk вітає внесок кожного. Ознайомтеся з [CONT
alt="Get it on F-Droid" alt="Get it on F-Droid"
height="80">](https://f-droid.org/en/packages/com.carriez.flutter_hbb) height="80">](https://f-droid.org/en/packages/com.carriez.flutter_hbb)
## Безкоштовні загальнодоступні сервери
Нижче наведені сервери, для безкоштовного використання, вони можуть змінюватися з часом. Якщо ви не перебуваєте поруч з одним із них, ваша мережа може працювати повільно.
| Місцезнаходження | Постачальник | Технічні характеристики |
| --------- | ------------- | ------------------ |
| Німеччина | [Hetzner](https://www.hetzner.com) | 2 vCPU / 4GB RAM |
| Україна (Київ) | [dc.volia](https://dc.volia.com) | 2 vCPU / 4GB RAM |
## Dev Container
[![Open in Dev Containers](https://img.shields.io/static/v1?label=Dev%20Container&message=Open&color=blue&logo=visualstudiocode)](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/rustdesk/rustdesk)
Якщо у вас уже встановлено VS Code та Docker, ви можете натиснути значок вище, щоб розпочати. Клацання призведе до того, що VS Code автоматично встановить розширення Dev Containers, якщо це необхідно, клонує вихідний код у том контейнера та розгорне контейнер dev для використання.
Дивіться [DEVCONTAINER.md](docs/DEVCONTAINER.md) для додаткової інформації
## Залежності ## Залежності
Стільничні версії використовують Flutter чи Sciter (застаріле) для графічного інтерфейсу. Ця інструкція лише для Sciter, оскільки він є більш простим та дружнім для початківців. Перегляньте [CI](https://github.com/rustdesk/rustdesk/blob/master/.github/workflows/flutter-build.yml) для збірки версії на Flutter. Стільничні версії використовують Flutter чи Sciter (застаріле) для графічного інтерфейсу. Ця інструкція лише для Sciter, оскільки він є більш простим та дружнім для початківців. Перегляньте [CI](https://github.com/rustdesk/rustdesk/blob/master/.github/workflows/flutter-build.yml) для збірки версії на Flutter.
@@ -77,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 libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libpam0g-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 sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel pam-devel
``` ```
### Fedora 28 (CentOS 8) ### 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 sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel gstreamer1-devel gstreamer1-plugins-base-devel pam-devel
``` ```
### Arch (Manjaro) ### Arch (Manjaro)
@@ -174,18 +156,22 @@ target/release/rustdesk
- **[libs/clipboard](https://github.com/rustdesk/rustdesk/tree/master/libs/clipboard)**: реалізація копіювання та вставлення файлів для Windows, Linux, macOS. - **[libs/clipboard](https://github.com/rustdesk/rustdesk/tree/master/libs/clipboard)**: реалізація копіювання та вставлення файлів для Windows, Linux, macOS.
- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: графічний інтерфейс користувача - **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: графічний інтерфейс користувача
- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: сервіси аудіо/буфера обміну/вводу/відео та мережевих підключень - **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: сервіси аудіо/буфера обміну/вводу/відео та мережевих підключень
- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: однорангове з'єднання - **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: однорангове зʼєднання
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: комунікація з [rustdesk-server](https://github.com/rustdesk/rustdesk-server), очікування віддаленого прямого (обхід TCP 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 веб клієнту - **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: JavaScript для веб клієнта на Flutter
## Знімки ## Знімки екрана
![image](https://user-images.githubusercontent.com/71636191/113112362-ae4deb80-923b-11eb-957d-ff88daad4f06.png) ![Менеджер зʼєднань](https://github.com/rustdesk/rustdesk/assets/28412477/db82d4e7-c4bc-4823-8e6f-6af7eadf7651)
![image](https://user-images.githubusercontent.com/71636191/113112619-f705a480-923b-11eb-911d-97e984ef52b6.png) ![Підключення до ПК з Windows](https://github.com/rustdesk/rustdesk/assets/28412477/9baa91e9-3362-4d06-aa1a-7518edcbd7ea)
![image](https://user-images.githubusercontent.com/71636191/113112857-3fbd5d80-923c-11eb-9836-768325faf906.png) ![Передача файлів](https://github.com/rustdesk/rustdesk/assets/28412477/39511ad3-aa9a-4f8c-8947-1cce286a46ad)
![image](https://user-images.githubusercontent.com/71636191/135385039-38fdbd72-379a-422d-b97f-33df71fb1cec.png) ![Тунелювання TCP](https://github.com/rustdesk/rustdesk/assets/28412477/78e8708f-e87e-4570-8373-1360033ea6c5)
## [Публічні сервери](#публічні-сервери)
RustDesk підтримується безкоштовним європейським сервером, любʼязно наданим [Codext GmbH](https://codext.link/rustdesk?utm_source=github)

View File

@@ -1,64 +1,55 @@
<p align="center"> <p align="center">
<img src="../res/logo-header.svg" alt="RustDesk - Phần mềm điểu khiển máy tính từ xa dành cho bạn"><br> <img src="../res/logo-header.svg" alt="RustDesk - Your remote desktop"><br>
<a href="#free-public-servers">Máy chủ</a> • <a href="#free-public-servers">Server</a> •
<a href="#raw-steps-to-build">Build</a> • <a href="#raw-steps-to-build">Build</a> •
<a href="#how-to-build-with-docker">Docker</a> • <a href="#how-to-build-with-docker">Docker</a> •
<a href="#file-structure">Cấu trúc tệp tin</a> • <a href="#file-structure">Structure</a> •
<a href="#snapshot">Snapshot</a><br> <a href="#snapshot">Snapshot</a><br>
[<a href="../README.md">English</a>] | [<a href="README-UA.md">Українська</a>] | [<a href="README-CS.md">česky</a>] | [<a href="README-ZH.md">中文</a>] | [<a href="README-HU.md">Magyar</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-GR.md">Ελληνικά</a>]<br> [<a href="../README.md">English</a>] | [<a href="README-UA.md">Українська</a>] | [<a href="README-CS.md">česky</a>] | [<a href="README-ZH.md">中文</a>] | [<a href="README-HU.md">Magyar</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-GR.md">Ελληνικά</a>]<br>
<b>Chúng tôi cần sự gíup đỡ của bạn để dịch trang README này, <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">RustDesk UI</a> và <a href="https://github.com/rustdesk/doc.rustdesk.com">tài liệu</a> sang ngôn ngữ bản địa của bạn</b> <b>Chúng tôi rất hoan nghênh sự hỗ trợ của bạn trong việc dịch trang README, trang giao diện người dùng của RustDesk - <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">RustDesk UI</a> và trang tài liệu của RustDesk - <a href="https://github.com/rustdesk/doc.rustdesk.com">RustDesk Doc</a> sang Tiếng Việt</b>
</p> </p>
Chat với chúng tôi qua: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) Hãy trao đổi với chúng tôi qua: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09) [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09)
Một phần mềm điểu khiển máy tính từ xa, đuợc lập trình bằng ngôn ngữ Rust. Hoạt động tức thì, không cần phải cài đặt. Bạn có toàn quyền điểu khiển với dữ liệu của bạn mà không cần phải lo lắng về sự bảo mật. Bạn có thể sử dụng máy chủ rendezvous/relay của chúng tôi, [tự cài đặt máy chủ](https://rustdesk.com/server), hay thậm chí [tự tạo máy chủ rendezvous/relay](https://github.com/rustdesk/rustdesk-server-demo). RustDesk là một phần mềm điểu khiển máy tính từ xa mã nguồn mở, được viết bằng Rust. Nó hoạt động ngay sau khi cài đặt, không yêu cầu cấu hình phức tạp. Bạn có toàn quyền kiểm soát với dữ liệu của mình mà không cần phải lo lắng về vấn đề bảo mật. Bạn có thể sử dụng máy chủ rendezvous/relay của chúng tôi hoặc [tự cài đặt máy chủ của riêng mình](https://rustdesk.com/server) hay thậm chí [tự tạo máy chủ rendezvous/relay cho riêng bạn](https://github.com/rustdesk/rustdesk-server-demo).
![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png) ![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png)
Mọi người đều đuợc chào đón để đóng góp vào RustDesk. Để bắt đầu, hãy đọc [`docs/CONTRIBUTING.md`](CONTRIBUTING.md). **RustDesk** luôn hoan nghênh mọi đóng góp từ mọi người. Hãy xem tệp [`docs/CONTRIBUTING.md`](CONTRIBUTING.md) để bắt đầu.
[**RustDesk hoạt động như thế nào?**](https://github.com/rustdesk/rustdesk/wiki/How-does-RustDesk-work%3F) [**FAQ**](https://github.com/rustdesk/rustdesk/wiki/FAQ)
[**BINARY DOWNLOAD**](https://github.com/rustdesk/rustdesk/releases)
[**CÁC BẢN PHÂN PHÁT MÃ NHỊ PHÂN**](https://github.com/rustdesk/rustdesk/releases) [**NIGHTLY BUILD**](https://github.com/rustdesk/rustdesk/FAQreleases/tag/nightly)
[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png" [<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png"
alt="Get it on F-Droid" alt="Get it on F-Droid"
height="80">](https://f-droid.org/en/packages/com.carriez.flutter_hbb) height="80">](https://f-droid.org/en/packages/com.carriez.flutter_hbb)
## Các Máy Chủ Công Khai Miễn Phí
Dưới đây là những máy chủ mà bạn có thể sử dụng mà không mất phí, chú ý là máy chủ có thể thay đổi theo thời gian. Nếu địa điểm của bạn không gần một trong số những máy chủ này, thì kết nói có thể chậm.
| Địa điểm | Nhà cung cấp | Cấu hình |
| --------- | ------------- | ------------------ |
| Germany | Hetzner | 2 vCPU / 4GB RAM |
## Dependencies ## Dependencies
Phiên bản cho máy tính sử dụng [sciter](https://sciter.com/) cho giao diện của phần mềm, vậy nên bn cần tự tải về thư viện sciter. Phiên bản máy tính sử dụng __Flutter__ hoặc __Sciter__ (đã lỗi thời) cho giao diện người dùng (GUI). Hướng dẫn này chỉ áp dụng cho phiên bn Sciter, vì nó thân thiện và dễ bắt đầu hơn. Hãy kiểm tra [CI](https://github.com/rustdesk/rustdesk/blob/master/.github/workflows/flutter-build.yml) của chúng tôi để xây dựng phiên bản Flutter.
[Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) | Vui lòng tự tải thư viện `Sciter` về máy theo hướng dẫn cho từng hệ điều hành.
[Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) |
[MacOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib)
Phiên bản cho điện thoại sử dụng Flutter. Chúng tôi sẽ chuyển sang sử dụng Flutter thay cho Sciter cho phiên bản máy tính. [Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) | [Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) | [MacOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib)
## Cách để build ## Các bước build cơ bản
- Chuẩn bị môi trường phát triển Rust và môi trường build C++ - Chuẩn bị môi trường phát triển Rust và môi trường biên dịch C++
- Tải và cài [vcpkg](https://github.com/microsoft/vcpkg), và đặt biến môi trường `VCPKG_ROOT` sao cho đúng. - Tải và cài đặt [`vcpkg`](https://github.com/microsoft/vcpkg), và thiết lập biến môi trường `VCPKG_ROOT`.
- Đối với Windows: vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static aom:x64-windows-static
- Đối với Linux/MacOS: vcpkg install libvpx libyuv opus aom
- Windows: `vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static aom:x64-windows-static`
- Linux/MacOS: `vcpkg install libvpx libyuv opus aom`
- Chạy lệnh `cargo run` - Chạy lệnh `cargo run`
## [Build](https://rustdesk.com/docs/en/dev/build/) ## [Build](https://rustdesk.com/docs/en/dev/build/)
## Cách để build cho Linux ## Cách build cho Linux
### Ubuntu 18 (Debian 10) ### Ubuntu 18 (Debian 10)
@@ -78,7 +69,7 @@ sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-
sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pipewire sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pipewire
``` ```
### Cách cài vcpkg ### Cách cài đặt `vcpkg`
```sh ```sh
git clone https://github.com/microsoft/vcpkg git clone https://github.com/microsoft/vcpkg
@@ -90,7 +81,7 @@ export VCPKG_ROOT=$HOME/vcpkg
vcpkg/vcpkg install libvpx libyuv opus aom vcpkg/vcpkg install libvpx libyuv opus aom
``` ```
### Cách sửa lỗi libvpx (Dành cho hệ điều hành Fedora) ### Cách sửa lỗi `libvpx` (Dành cho hệ điều hành Fedora)
```sh ```sh
cd vcpkg/buildtrees/libvpx/src cd vcpkg/buildtrees/libvpx/src
@@ -103,7 +94,7 @@ cp libvpx.a $HOME/vcpkg/installed/x64-linux/lib/
cd cd
``` ```
### Cách build ### Build
```sh ```sh
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
@@ -116,9 +107,9 @@ mv libsciter-gtk.so target/debug
VCPKG_ROOT=$HOME/vcpkg cargo run VCPKG_ROOT=$HOME/vcpkg cargo run
``` ```
## Cách để build sử dụng Docker ## Cách build bằng Docker
Bắt đầu bằng cách sao chép repo này về máy tính và build cái Docker cointainer: Bắt đầu bằng cách sao chép repo này về máy tính của bạn và tạo Docker container:
```sh ```sh
git clone https://github.com/rustdesk/rustdesk git clone https://github.com/rustdesk/rustdesk
@@ -126,37 +117,37 @@ cd rustdesk
docker build -t "rustdesk-builder" . docker build -t "rustdesk-builder" .
``` ```
Rồi mỗi khi bạn chạy ứng dụng, thì hãy chạy lệnh này: Sau đó, mỗi khi bạn chạy ứng dụng, thì hãy chạy dòng lệnh sau:
```sh ```sh
docker run --rm -it -v $PWD:/home/user/rustdesk -v rustdesk-git-cache:/home/user/.cargo/git -v rustdesk-registry-cache:/home/user/.cargo/registry -e PUID="$(id -u)" -e PGID="$(id -g)" rustdesk-builder docker run --rm -it -v $PWD:/home/user/rustdesk -v rustdesk-git-cache:/home/user/.cargo/git -v rustdesk-registry-cache:/home/user/.cargo/registry -e PUID="$(id -u)" -e PGID="$(id -g)" rustdesk-builder
``` ```
Chú ý: Lần build đầu tiên có thể sẽ mất lâu hơn truớc khi các dependecies đuợc lưu lại, nhng lần build sau sẽ nhanh hơn. Hơn nũa, nếu bạn cần cung cấp các cài đặt lệnh khác cho lệnh build, bạn có thể đặt những cài đặt lệnh này vào cuối lệnh ở phần `<OPTIONAL-ARGS>`. Ví dụ nếu bạn cần build phiên bản đuợc tối ưu hóa, bạn sẽ chạy lệnh trên cùng với cài đặt lệnh --release. Kết quả build sẽ được lưu trong thư mục target trên máy tính của bạn, và có thể chạy với lệnh: Lưu ý rằng **lần build đầu tiên có thể mất thời gian hơn trước khi các dependencies được lưu vào bộ nhớ cache**, nhưng các lần build sau sẽ nhanh hơn. Ngoài ra, nếu bạn cần chỉ định các đối số khác cho lệnh build, bạn có thể thêm chúng vào cuối lệnh ở phần `<OPTIONAL-ARGS>`. Ví dụ, nếu bạn muốn build phiên bản tối ưu hóa, bạn sẽ chạy lệnh trên với tùy chọn `--release`. Kết quả biên dịch sẽ được lưu trong thư mục target trên máy tính của bạn, và có thể chạy với lệnh:
```sh ```sh
target/debug/rustdesk target/debug/rustdesk
``` ```
Nếu bạn đang chạy bản build đuợc tối ưu hóa, thì bạn có thể chạy với lệnh: Nếu bạn đang chạy bản build được tối ưu hóa, thì bạn có thể chạy với lệnh:
```sh ```sh
target/release/rustdesk target/release/rustdesk
``` ```
Hãy đảm bảo bạn đang chạy những lệnh này từ thu mục rễ của repo RustDesk, vì nếu không thì ứng dụng có thể sẽ không tìm đuợc những tệp tài nguyên cần thiết. Cũng như nhớ rằng những lệnh con của cargo như `install` hoặc `run` hiện chưa được hỗ trợ bởi phương pháp này vì chúng sẽ cài đặt hoặc chạy ứng dụng trong container thay vì trên máy tính của bạn. Hãy đảm bảo rằng bạn đang chạy các lệnh này từ gốc của thư mục **RustDesk**, nếu không, ứng dụng có thể không thể tìm thấy các tệp tài nguyên cần thiết. Hãy lưu ý rằng các câu lệnh con khác của **cargo** như **install** hoặc **run** hiện không được hỗ trợ qua phương pháp này, vì chúng sẽ cài đặt hoặc chạy chương trình bên trong **container** thay vì trên máy tính của bạn.
## Cấu trúc tệp tin ## Cấu trúc tệp tin
- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: video codec, cấu hình, tcp/udp wrapper, protobuf, fs functions để truyền file, và một số hàm tiện ích khác - **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: video codec, cấu hình, tcp/udp wrapper, protobuf, fs functions để truyền file, và một số hàm tiện ích khác
- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: để ghi lại màn hình - **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: ghi lại màn hình
- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: để điều khiển máy tính/con chuột trên những nền tảng khác nhau - **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: điều khiển máy tính/chuột trên các nền tảng khác nhau
- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: giao diện người dùng - **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: giao diện người dùng
- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: các dịch vụ âm thanh, clipboard, đầu vào, video và các kết nối mạng - **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: các dịch vụ âm thanh, clipboard, đầu vào, video và các kết nối mạng
- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: để bắt đầu kết nối với một peer - **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: bắt đầu kết nối với một peer
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Để liên lạc với [rustdesk-server](https://github.com/rustdesk/rustdesk-server), đợi cho kết nối trực tiếp (TCP hole punching) hoặc kết nối được relayed. - **[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 cho đ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 - **[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

View File

@@ -8,7 +8,7 @@
[<a href="../README.md">English</a>] | [<a href="README-UA.md">Українська</a>] | [<a href="README-CS.md">česky</a>] | [<a href="README-HU.md">Magyar</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-VN.md">Tiếng Việt</a>] | [<a href="README-GR.md">Ελληνικά</a>]<br> [<a href="../README.md">English</a>] | [<a href="README-UA.md">Українська</a>] | [<a href="README-CS.md">česky</a>] | [<a href="README-HU.md">Magyar</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-VN.md">Tiếng Việt</a>] | [<a href="README-GR.md">Ελληνικά</a>]<br>
</p> </p>
Chat with us: [知乎](https://www.zhihu.com/people/rustdesk) | [Discord](https://discord.gg/nDceKgxnkV) | [Reddit](https://www.reddit.com/r/rustdesk) 与我们交流: [知乎](https://www.zhihu.com/people/rustdesk) | [Discord](https://discord.gg/nDceKgxnkV) | [Reddit](https://www.reddit.com/r/rustdesk)
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09) [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09)
@@ -18,7 +18,7 @@ Chat with us: [知乎](https://www.zhihu.com/people/rustdesk) | [Discord](https:
![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png) ![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png)
RustDesk 期待各位的贡献. 如何参与开发? 详情请看 [CONTRIBUTING.md](CONTRIBUTING.md). RustDesk 期待各位的贡献. 如何参与开发? 详情请看 [CONTRIBUTING-ZH.md](CONTRIBUTING-ZH.md).
[**FAQ**](https://github.com/rustdesk/rustdesk/wiki/FAQ) [**FAQ**](https://github.com/rustdesk/rustdesk/wiki/FAQ)
@@ -30,26 +30,11 @@ RustDesk 期待各位的贡献. 如何参与开发? 详情请看 [CONTRIBUTING.m
alt="Get it on F-Droid" alt="Get it on F-Droid"
height="80">](https://f-droid.org/en/packages/com.carriez.flutter_hbb) height="80">](https://f-droid.org/en/packages/com.carriez.flutter_hbb)
## 免费的公共服务器
以下是您可以使用的、免费的、会随时更新的公共服务器列表,在国内也许网速会很慢或者无法访问。
| Location | Vendor | Specification |
| --------- | ------------- | ------------------ |
| Germany | [Hetzner](https://www.hetzner.com) | 2 vCPU / 4 GB RAM |
| Ukraine (Kyiv) | [dc.volia](https://dc.volia.com) | 2 vCPU / 4 GB RAM |
## Dev Container
[![在 Dev Containers 中打开](https://img.shields.io/static/v1?label=Dev%20Container&message=Open&color=blue&logo=visualstudiocode)](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/rustdesk/rustdesk)
如果你已经安装了 VS Code 和 Docker, 你可以点击上面的徽章开始使用. 点击后, VS Code 将自动安装 Dev Containers 扩展(如果需要),将源代码克隆到容器卷中, 并启动一个 Dev 容器供使用.
Go through [DEVCONTAINER.md](docs/DEVCONTAINER.md) for more info.
## 依赖 ## 依赖
桌面版本界面使用[sciter](https://sciter.com/), 请自行下载 桌面版本使用 Flutter 或 Sciter已弃用作为 GUI本教程仅适用于 Sciter因为它更简单且更易于上手。查看我们的[CI](https://github.com/rustdesk/rustdesk/blob/master/.github/workflows/flutter-build.yml)以构建 Flutter 版本
请自行下载Sciter动态库。
[Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) | [Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) |
[Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) | [Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) |
@@ -224,12 +209,13 @@ target/release/rustdesk
- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: 视频编解码, 配置, tcp/udp 封装, protobuf, 文件传输相关文件系统操作函数, 以及一些其他实用函数 - **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: 视频编解码, 配置, tcp/udp 封装, protobuf, 文件传输相关文件系统操作函数, 以及一些其他实用函数
- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: 屏幕截取 - **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: 屏幕截取
- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: 平台相关的鼠标键盘输入 - **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: 平台相关的鼠标键盘输入
- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: GUI - **[libs/clipboard](https://github.com/rustdesk/rustdesk/tree/master/libs/clipboard)**: Windows、Linux、macOS 的文件复制和粘贴实现
- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: 过时的 Sciter UI已弃用
- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: 被控端服务音频、剪切板、输入、视频服务、网络连接的实现 - **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: 被控端服务音频、剪切板、输入、视频服务、网络连接的实现
- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: 控制端 - **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: 控制端
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: 与[rustdesk-server](https://github.com/rustdesk/rustdesk-server)保持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代码 - **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: Flutter Web版本中的Javascript代码
## 截图 ## 截图

View File

@@ -8,29 +8,43 @@
"modules": [ "modules": [
"shared-modules/libappindicator/libappindicator-gtk3-12.10.json", "shared-modules/libappindicator/libappindicator-gtk3-12.10.json",
"xdotool.json", "xdotool.json",
{
"name": "pam",
"buildsystem": "simple",
"build-commands": [
"./configure --disable-selinux --prefix=/app && make -j4 install"
],
"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",
"build-commands": [ "build-commands": [
"bsdtar -zxvf rustdesk-1.2.4.deb", "bsdtar -zxvf rustdesk.deb",
"tar -xvf ./data.tar.xz", "tar -xvf ./data.tar.xz",
"cp -r ./usr/* /app/", "cp -r ./usr/* /app/",
"mkdir -p /app/bin && ln -s /app/lib/rustdesk/rustdesk /app/bin/rustdesk", "mkdir -p /app/bin && ln -s /app/lib/rustdesk/rustdesk /app/bin/rustdesk",
"mv /app/share/applications/rustdesk.desktop /app/share/applications/com.rustdesk.RustDesk.desktop", "mv /app/share/applications/rustdesk.desktop /app/share/applications/com.rustdesk.RustDesk.desktop",
"sed -i '/^Icon=/ c\\Icon=com.rustdesk.RustDesk' /app/share/applications/com.rustdesk.RustDesk.desktop", "mv /app/share/applications/rustdesk-link.desktop /app/share/applications/com.rustdesk.RustDesk-link.desktop",
"sed -i '/^Icon=/ c\\Icon=com.rustdesk.RustDesk' /app/share/applications/rustdesk-link.desktop", "sed -i '/^Icon=/ c\\Icon=com.rustdesk.RustDesk' /app/share/applications/*.desktop",
"mv /app/share/icons/hicolor/scalable/apps/rustdesk.svg /app/share/icons/hicolor/scalable/apps/com.rustdesk.RustDesk.svg", "mv /app/share/icons/hicolor/scalable/apps/rustdesk.svg /app/share/icons/hicolor/scalable/apps/com.rustdesk.RustDesk.svg",
"for size in 16 24 32 48 64 128 256 512; do\n rsvg-convert -w $size -h $size -f png -o $size.png logo.svg\n install -Dm644 $size.png /app/share/icons/hicolor/${size}x${size}/apps/com.rustdesk.RustDesk.png\n done" "for size in 16 24 32 48 64 128 256 512; do\n rsvg-convert -w $size -h $size -f png -o $size.png scalable.svg\n install -Dm644 $size.png /app/share/icons/hicolor/${size}x${size}/apps/com.rustdesk.RustDesk.png\n done"
], ],
"cleanup": ["/include", "/lib/pkgconfig", "/share/gtk-doc"], "cleanup": ["/include", "/lib/pkgconfig", "/share/gtk-doc"],
"sources": [ "sources": [
{ {
"type": "file", "type": "file",
"path": "../rustdesk-1.2.4.deb" "path": "./rustdesk.deb"
}, },
{ {
"type": "file", "type": "file",
"path": "../res/logo.svg" "path": "../res/scalable.svg"
} }
] ]
} }

1
flutter/.gitignore vendored
View File

@@ -32,7 +32,6 @@
/build/ /build/
# Web related # Web related
lib/generated_plugin_registrant.dart
# Symbolication related # Symbolication related
app.*.symbols app.*.symbols

View File

@@ -106,6 +106,6 @@ dependencies {
implementation "androidx.media:media:1.6.0" implementation "androidx.media:media:1.6.0"
implementation 'com.github.getActivity:XXPermissions:18.5' implementation 'com.github.getActivity:XXPermissions:18.5'
implementation("org.jetbrains.kotlin:kotlin-stdlib") { version { strictly("$kotlin_version") } } implementation("org.jetbrains.kotlin:kotlin-stdlib") { version { strictly("$kotlin_version") } }
implementation 'com.caverock:androidsvg-aar:1.4'
} }
apply plugin: 'com.google.gms.google-services'

View File

@@ -1,40 +0,0 @@
{
"project_info": {
"project_number": "768133699366",
"firebase_url": "https://rustdesk.firebaseio.com",
"project_id": "rustdesk",
"storage_bucket": "rustdesk.appspot.com"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:768133699366:android:5fc9015370e344457993e7",
"android_client_info": {
"package_name": "com.carriez.flutter_hbb"
}
},
"oauth_client": [
{
"client_id": "768133699366-s9gdfsijefsd5g1nura4kmfne42lencn.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyAPOsKcXjrAR-7Z148sYr_gdB_JQZkamTM"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "768133699366-s9gdfsijefsd5g1nura4kmfne42lencn.apps.googleusercontent.com",
"client_type": 3
}
]
}
}
}
],
"configuration_version": "1"
}

View File

@@ -3,6 +3,7 @@
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" />
<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.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
@@ -80,6 +81,11 @@
android:name=".MainService" android:name=".MainService"
android:enabled="true" android:enabled="true"
android:foregroundServiceType="mediaProjection" /> android:foregroundServiceType="mediaProjection" />
<service
android:name=".FloatingWindowService"
android:enabled="true" />
<!-- <!--
Don't delete the meta-data below. Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java This is used by the Flutter tool to generate GeneratedPluginRegistrant.java

View File

@@ -0,0 +1,193 @@
package com.carriez.flutter_hbb
import ffi.FFI
import android.Manifest
import android.content.Context
import android.media.*
import android.content.pm.PackageManager
import android.media.projection.MediaProjection
import androidx.annotation.RequiresApi
import androidx.core.app.ActivityCompat
import android.os.Build
import android.util.Log
import kotlin.concurrent.thread
const val AUDIO_ENCODING = AudioFormat.ENCODING_PCM_FLOAT // ENCODING_OPUS need API 30
const val AUDIO_SAMPLE_RATE = 48000
const val AUDIO_CHANNEL_MASK = AudioFormat.CHANNEL_IN_STEREO
class AudioRecordHandle(private var context: Context, private var isVideoStart: ()->Boolean, private var isAudioStart: ()->Boolean) {
private val logTag = "LOG_AUDIO_RECORD_HANDLE"
private var audioRecorder: AudioRecord? = null
private var audioReader: AudioReader? = null
private var minBufferSize = 0
private var audioRecordStat = false
private var audioThread: Thread? = null
@RequiresApi(Build.VERSION_CODES.M)
fun createAudioRecorder(inVoiceCall: Boolean, mediaProjection: MediaProjection?): Boolean {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
return false
}
if (ActivityCompat.checkSelfPermission(
context,
Manifest.permission.RECORD_AUDIO
) != PackageManager.PERMISSION_GRANTED
) {
Log.d(logTag, "createAudioRecorder failed, no RECORD_AUDIO permission")
return false
}
var builder = AudioRecord.Builder()
.setAudioFormat(
AudioFormat.Builder()
.setEncoding(AUDIO_ENCODING)
.setSampleRate(AUDIO_SAMPLE_RATE)
.setChannelMask(AUDIO_CHANNEL_MASK).build()
);
if (inVoiceCall) {
builder.setAudioSource(MediaRecorder.AudioSource.VOICE_COMMUNICATION)
} else {
mediaProjection?.let {
var apcc = AudioPlaybackCaptureConfiguration.Builder(it)
.addMatchingUsage(AudioAttributes.USAGE_MEDIA)
.addMatchingUsage(AudioAttributes.USAGE_ALARM)
.addMatchingUsage(AudioAttributes.USAGE_GAME)
.addMatchingUsage(AudioAttributes.USAGE_UNKNOWN).build();
builder.setAudioPlaybackCaptureConfig(apcc);
} ?: let {
Log.d(logTag, "createAudioRecorder failed, mediaProjection null")
return false
}
}
audioRecorder = builder.build()
Log.d(logTag, "createAudioRecorder done,minBufferSize:$minBufferSize")
return true
}
@RequiresApi(Build.VERSION_CODES.M)
private fun checkAudioReader() {
if (audioReader != null && minBufferSize != 0) {
return
}
// read f32 to byte , length * 4
minBufferSize = 2 * 4 * AudioRecord.getMinBufferSize(
AUDIO_SAMPLE_RATE,
AUDIO_CHANNEL_MASK,
AUDIO_ENCODING
)
if (minBufferSize == 0) {
Log.d(logTag, "get min buffer size fail!")
return
}
audioReader = AudioReader(minBufferSize, 4)
Log.d(logTag, "init audioData len:$minBufferSize")
}
@RequiresApi(Build.VERSION_CODES.M)
fun startAudioRecorder() {
checkAudioReader()
if (audioReader != null && audioRecorder != null && minBufferSize != 0) {
try {
FFI.setFrameRawEnable("audio", true)
audioRecorder!!.startRecording()
audioRecordStat = true
audioThread = thread {
while (audioRecordStat) {
audioReader!!.readSync(audioRecorder!!)?.let {
FFI.onAudioFrameUpdate(it)
}
}
// let's release here rather than onDestroy to avoid threading issue
audioRecorder?.release()
audioRecorder = null
minBufferSize = 0
FFI.setFrameRawEnable("audio", false)
Log.d(logTag, "Exit audio thread")
}
} catch (e: Exception) {
Log.d(logTag, "startAudioRecorder fail:$e")
}
} else {
Log.d(logTag, "startAudioRecorder fail")
}
}
fun onVoiceCallStarted(mediaProjection: MediaProjection?): Boolean {
if (!isSupportVoiceCall()) {
return false
}
// No need to check if video or audio is started here.
if (!switchToVoiceCall(mediaProjection)) {
return false
}
return true
}
fun onVoiceCallClosed(mediaProjection: MediaProjection?): Boolean {
// Return true if not supported, because is was not started.
if (!isSupportVoiceCall()) {
return true
}
if (isVideoStart()) {
switchOutVoiceCall(mediaProjection)
}
tryReleaseAudio()
return true
}
@RequiresApi(Build.VERSION_CODES.M)
fun switchToVoiceCall(mediaProjection: MediaProjection?): Boolean {
audioRecorder?.let {
if (it.getAudioSource() == MediaRecorder.AudioSource.VOICE_COMMUNICATION) {
return true
}
}
audioRecordStat = false
audioThread?.join()
audioThread = null
if (!createAudioRecorder(true, mediaProjection)) {
Log.e(logTag, "createAudioRecorder fail")
return false
}
startAudioRecorder()
return true
}
@RequiresApi(Build.VERSION_CODES.M)
fun switchOutVoiceCall(mediaProjection: MediaProjection?): Boolean {
audioRecorder?.let {
if (it.getAudioSource() != MediaRecorder.AudioSource.VOICE_COMMUNICATION) {
return true
}
}
audioRecordStat = false
audioThread?.join()
if (!createAudioRecorder(false, mediaProjection)) {
Log.e(logTag, "createAudioRecorder fail")
return false
}
startAudioRecorder()
return true
}
fun tryReleaseAudio() {
if (isAudioStart() || isVideoStart()) {
return
}
audioRecordStat = false
audioThread?.join()
audioThread = null
}
fun destroy() {
Log.d(logTag, "destroy audio record handle")
audioRecordStat = false
audioThread?.join()
}
}

View File

@@ -0,0 +1,378 @@
package com.carriez.flutter_hbb
import android.annotation.SuppressLint
import android.app.PendingIntent
import android.app.Service
import android.content.Intent
import android.content.res.Configuration
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.PixelFormat
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable
import android.os.Build
import android.os.Handler
import android.os.IBinder
import android.os.Looper
import android.util.Log
import android.view.Gravity
import android.view.MotionEvent
import android.view.View
import android.view.WindowManager
import android.view.WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
import android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
import android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
import android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
import android.widget.ImageView
import android.widget.PopupMenu
import com.caverock.androidsvg.SVG
import ffi.FFI
import kotlin.math.abs
class FloatingWindowService : Service(), View.OnTouchListener {
private lateinit var windowManager: WindowManager
private lateinit var layoutParams: WindowManager.LayoutParams
private lateinit var floatingView: ImageView
private lateinit var originalDrawable: Drawable
private lateinit var leftHalfDrawable: Drawable
private lateinit var rightHalfDrawable: Drawable
private var dragging = false
private var lastDownX = 0f
private var lastDownY = 0f
private var viewCreated = false;
private var keepScreenOn = KeepScreenOn.DURING_CONTROLLED
companion object {
private val logTag = "floatingService"
private var firstCreate = true
private var viewWidth = 120
private var viewHeight = 120
private const val MIN_VIEW_SIZE = 32 // size 0 does not help prevent the service from being killed
private const val MAX_VIEW_SIZE = 320
private var viewUntouchable = false
private var viewTransparency = 1f // 0 means invisible but can help prevent the service from being killed
private var customSvg = ""
private var lastLayoutX = 0
private var lastLayoutY = 0
private var lastOrientation = Configuration.ORIENTATION_UNDEFINED
}
override fun onBind(intent: Intent): IBinder? {
return null
}
override fun onCreate() {
super.onCreate()
windowManager = getSystemService(WINDOW_SERVICE) as WindowManager
try {
if (firstCreate) {
firstCreate = false
onFirstCreate(windowManager)
}
Log.d(logTag, "floating window size: $viewWidth x $viewHeight, transparency: $viewTransparency, lastLayoutX: $lastLayoutX, lastLayoutY: $lastLayoutY, customSvg: $customSvg")
createView(windowManager)
handler.postDelayed(runnable, 1000)
Log.d(logTag, "onCreate success")
} catch (e: Exception) {
Log.d(logTag, "onCreate failed: $e")
}
}
override fun onDestroy() {
super.onDestroy()
if (viewCreated) {
windowManager.removeView(floatingView)
}
handler.removeCallbacks(runnable)
}
@SuppressLint("ClickableViewAccessibility")
private fun createView(windowManager: WindowManager) {
floatingView = ImageView(this)
viewCreated = true
originalDrawable = resources.getDrawable(R.drawable.floating_window, null)
if (customSvg.isNotEmpty()) {
try {
val svg = SVG.getFromString(customSvg)
Log.d(logTag, "custom svg info: ${svg.documentWidth} x ${svg.documentHeight}");
// This make the svg render clear
svg.documentWidth = viewWidth * 1f
svg.documentHeight = viewHeight * 1f
originalDrawable = svg.renderToPicture().let {
BitmapDrawable(
resources,
Bitmap.createBitmap(it.width, it.height, Bitmap.Config.ARGB_8888)
.also { bitmap ->
it.draw(Canvas(bitmap))
})
}
floatingView.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
Log.d(logTag, "custom svg loaded")
} catch (e: Exception) {
e.printStackTrace()
}
}
val originalBitmap = Bitmap.createBitmap(
originalDrawable.intrinsicWidth,
originalDrawable.intrinsicHeight,
Bitmap.Config.ARGB_8888
)
val canvas = Canvas(originalBitmap)
originalDrawable.setBounds(
0,
0,
originalDrawable.intrinsicWidth,
originalDrawable.intrinsicHeight
)
originalDrawable.draw(canvas)
val leftHalfBitmap = Bitmap.createBitmap(
originalBitmap,
0,
0,
originalDrawable.intrinsicWidth / 2,
originalDrawable.intrinsicHeight
)
val rightHalfBitmap = Bitmap.createBitmap(
originalBitmap,
originalDrawable.intrinsicWidth / 2,
0,
originalDrawable.intrinsicWidth / 2,
originalDrawable.intrinsicHeight
)
leftHalfDrawable = BitmapDrawable(resources, leftHalfBitmap)
rightHalfDrawable = BitmapDrawable(resources, rightHalfBitmap)
floatingView.setImageDrawable(rightHalfDrawable)
floatingView.setOnTouchListener(this)
floatingView.alpha = viewTransparency * 1f
var flags = FLAG_LAYOUT_IN_SCREEN or FLAG_NOT_TOUCH_MODAL or FLAG_NOT_FOCUSABLE
if (viewUntouchable || viewTransparency == 0f) {
flags = flags or WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
}
layoutParams = WindowManager.LayoutParams(
viewWidth / 2,
viewHeight,
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY else WindowManager.LayoutParams.TYPE_PHONE,
flags,
PixelFormat.TRANSLUCENT
)
layoutParams.gravity = Gravity.TOP or Gravity.START
layoutParams.x = lastLayoutX
layoutParams.y = lastLayoutY
val keepScreenOnOption = FFI.getLocalOption("keep-screen-on").lowercase()
keepScreenOn = when (keepScreenOnOption) {
"never" -> KeepScreenOn.NEVER
"service-on" -> KeepScreenOn.SERVICE_ON
else -> KeepScreenOn.DURING_CONTROLLED
}
Log.d(logTag, "keepScreenOn option: $keepScreenOnOption, value: $keepScreenOn")
updateKeepScreenOnLayoutParams()
windowManager.addView(floatingView, layoutParams)
moveToScreenSide()
}
private fun onFirstCreate(windowManager: WindowManager) {
val wh = getScreenSize(windowManager)
val w = wh.first
val h = wh.second
// size
FFI.getLocalOption("floating-window-size").let {
if (it.isNotEmpty()) {
try {
val size = it.toInt()
if (size in MIN_VIEW_SIZE..MAX_VIEW_SIZE && size <= w / 2 && size <= h / 2) {
viewWidth = size
viewHeight = size
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}
// untouchable
viewUntouchable = FFI.getLocalOption("floating-window-untouchable") == "Y"
// transparency
FFI.getLocalOption("floating-window-transparency").let {
if (it.isNotEmpty()) {
try {
val transparency = it.toInt()
if (transparency in 0..10) {
viewTransparency = transparency * 1f / 10
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}
// custom svg
FFI.getLocalOption("floating-window-svg").let {
if (it.isNotEmpty()) {
customSvg = it
}
}
// position
lastLayoutX = 0
lastLayoutY = (wh.second - viewHeight) / 2
lastOrientation = resources.configuration.orientation
}
private fun performClick() {
showPopupMenu()
}
override fun onTouch(view: View?, event: MotionEvent?): Boolean {
when (event?.action) {
MotionEvent.ACTION_DOWN -> {
dragging = false
lastDownX = event.rawX
lastDownY = event.rawY
}
MotionEvent.ACTION_UP -> {
val clickDragTolerance = 10f
if (abs(event.rawX - lastDownX) < clickDragTolerance && abs(event.rawY - lastDownY) < clickDragTolerance) {
performClick()
} else {
moveToScreenSide()
}
}
MotionEvent.ACTION_MOVE -> {
val dx = event.rawX - lastDownX
val dy = event.rawY - lastDownY
// ignore too small fist start moving(some time is click)
if (!dragging && dx*dx+dy*dy < 25) {
return false
}
dragging = true
layoutParams.x = event.rawX.toInt()
layoutParams.y = event.rawY.toInt()
layoutParams.width = viewWidth
floatingView.setImageDrawable(originalDrawable)
windowManager.updateViewLayout(view, layoutParams)
lastLayoutX = layoutParams.x
lastLayoutY = layoutParams.y
}
}
return false
}
private fun moveToScreenSide(center: Boolean = false) {
val windowManager = getSystemService(WINDOW_SERVICE) as WindowManager
val wh = getScreenSize(windowManager)
val w = wh.first
if (layoutParams.x < w / 2) {
layoutParams.x = 0
floatingView.setImageDrawable(rightHalfDrawable)
} else {
layoutParams.x = w - viewWidth / 2
floatingView.setImageDrawable(leftHalfDrawable)
}
if (center) {
layoutParams.y = (wh.second - viewHeight) / 2
}
layoutParams.width = viewWidth / 2
windowManager.updateViewLayout(floatingView, layoutParams)
lastLayoutX = layoutParams.x
lastLayoutY = layoutParams.y
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
if (newConfig.orientation != lastOrientation) {
lastOrientation = newConfig.orientation
val wh = getScreenSize(windowManager)
Log.d(logTag, "orientation: $lastOrientation, screen size: ${wh.first} x ${wh.second}")
val newW = wh.first
val newH = wh.second
if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE || newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) {
// Proportional change
layoutParams.x = (layoutParams.x.toFloat() / newH.toFloat() * newW.toFloat()).toInt()
layoutParams.y = (layoutParams.y.toFloat() / newW.toFloat() * newH.toFloat()).toInt()
}
moveToScreenSide()
}
}
private fun showPopupMenu() {
val popupMenu = PopupMenu(this, floatingView)
val idShowRustDesk = 0
popupMenu.menu.add(0, idShowRustDesk, 0, translate("Show RustDesk"))
val idStopService = 1
popupMenu.menu.add(0, idStopService, 0, translate("Stop service"))
popupMenu.setOnMenuItemClickListener { menuItem ->
when (menuItem.itemId) {
idShowRustDesk -> {
openMainActivity()
true
}
idStopService -> {
stopMainService()
true
}
else -> false
}
}
popupMenu.setOnDismissListener {
moveToScreenSide()
}
popupMenu.show()
}
private fun openMainActivity() {
val intent = Intent(this, MainActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
val pendingIntent = PendingIntent.getActivity(
this, 0, intent,
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_ONE_SHOT
)
try {
pendingIntent.send()
} catch (e: PendingIntent.CanceledException) {
e.printStackTrace()
}
}
private fun stopMainService() {
MainActivity.flutterMethodChannel?.invokeMethod("stop_service", null)
}
enum class KeepScreenOn {
NEVER,
DURING_CONTROLLED,
SERVICE_ON,
}
private val handler = Handler(Looper.getMainLooper())
private val runnable = object : Runnable {
override fun run() {
if (updateKeepScreenOnLayoutParams()) {
windowManager.updateViewLayout(floatingView, layoutParams)
}
handler.postDelayed(this, 1000) // 1000 milliseconds = 1 second
}
}
private fun updateKeepScreenOnLayoutParams(): Boolean {
val oldOn = layoutParams.flags and FLAG_KEEP_SCREEN_ON != 0
val newOn = keepScreenOn == KeepScreenOn.SERVICE_ON || (keepScreenOn == KeepScreenOn.DURING_CONTROLLED && MainService.isStart)
if (oldOn != newOn) {
Log.d(logTag, "change keep screen on to $newOn")
if (newOn) {
layoutParams.flags = layoutParams.flags or FLAG_KEEP_SCREEN_ON
} else {
layoutParams.flags = layoutParams.flags and FLAG_KEEP_SCREEN_ON.inv()
}
return true
}
return false
}
}

View File

@@ -18,7 +18,9 @@ import android.widget.EditText
import android.view.accessibility.AccessibilityEvent import android.view.accessibility.AccessibilityEvent
import android.view.ViewGroup.LayoutParams import android.view.ViewGroup.LayoutParams
import android.view.accessibility.AccessibilityNodeInfo import android.view.accessibility.AccessibilityNodeInfo
import android.view.KeyEvent as KeyEventAndroid
import android.graphics.Rect import android.graphics.Rect
import android.media.AudioManager
import android.accessibilityservice.AccessibilityServiceInfo import android.accessibilityservice.AccessibilityServiceInfo
import android.accessibilityservice.AccessibilityServiceInfo.FLAG_INPUT_METHOD_EDITOR import android.accessibilityservice.AccessibilityServiceInfo.FLAG_INPUT_METHOD_EDITOR
import android.accessibilityservice.AccessibilityServiceInfo.FLAG_RETRIEVE_INTERACTIVE_WINDOWS import android.accessibilityservice.AccessibilityServiceInfo.FLAG_RETRIEVE_INTERACTIVE_WINDOWS
@@ -75,6 +77,8 @@ class InputService : AccessibilityService() {
private var fakeEditTextForTextStateCalculation: EditText? = null private var fakeEditTextForTextStateCalculation: EditText? = null
private val volumeController: VolumeController by lazy { VolumeController(applicationContext.getSystemService(AUDIO_SERVICE) as AudioManager) }
@RequiresApi(Build.VERSION_CODES.N) @RequiresApi(Build.VERSION_CODES.N)
fun onMouseInput(mask: Int, _x: Int, _y: Int) { fun onMouseInput(mask: Int, _x: Int, _y: Int) {
val x = max(0, _x) val x = max(0, _x)
@@ -294,6 +298,18 @@ class InputService : AccessibilityService() {
Log.d(logTag, "onKeyEvent $keyEvent textToCommit:$textToCommit") Log.d(logTag, "onKeyEvent $keyEvent textToCommit:$textToCommit")
var ke: KeyEventAndroid? = null
if (Build.VERSION.SDK_INT < 33 || textToCommit == null) {
ke = KeyEventConverter.toAndroidKeyEvent(keyEvent)
}
ke?.let { event ->
if (tryHandleVolumeKeyEvent(event)) {
return
} else if (tryHandlePowerKeyEvent(event)) {
return
}
}
if (Build.VERSION.SDK_INT >= 33) { if (Build.VERSION.SDK_INT >= 33) {
getInputMethod()?.let { inputMethod -> getInputMethod()?.let { inputMethod ->
inputMethod.getCurrentInputConnection()?.let { inputConnection -> inputMethod.getCurrentInputConnection()?.let { inputConnection ->
@@ -302,7 +318,7 @@ class InputService : AccessibilityService() {
inputConnection.commitText(text, 1, null) inputConnection.commitText(text, 1, null)
} }
} else { } else {
KeyEventConverter.toAndroidKeyEvent(keyEvent).let { event -> ke?.let { event ->
inputConnection.sendKeyEvent(event) inputConnection.sendKeyEvent(event)
} }
} }
@@ -311,7 +327,7 @@ class InputService : AccessibilityService() {
} else { } else {
val handler = Handler(Looper.getMainLooper()) val handler = Handler(Looper.getMainLooper())
handler.post { handler.post {
KeyEventConverter.toAndroidKeyEvent(keyEvent)?.let { event -> ke?.let { event ->
val possibleNodes = possibleAccessibiltyNodes() val possibleNodes = possibleAccessibiltyNodes()
Log.d(logTag, "possibleNodes:$possibleNodes") Log.d(logTag, "possibleNodes:$possibleNodes")
for (item in possibleNodes) { for (item in possibleNodes) {
@@ -325,6 +341,43 @@ class InputService : AccessibilityService() {
} }
} }
private fun tryHandleVolumeKeyEvent(event: KeyEventAndroid): Boolean {
when (event.keyCode) {
KeyEventAndroid.KEYCODE_VOLUME_UP -> {
if (event.action == KeyEventAndroid.ACTION_DOWN) {
volumeController.raiseVolume(null, true, AudioManager.STREAM_SYSTEM)
}
return true
}
KeyEventAndroid.KEYCODE_VOLUME_DOWN -> {
if (event.action == KeyEventAndroid.ACTION_DOWN) {
volumeController.lowerVolume(null, true, AudioManager.STREAM_SYSTEM)
}
return true
}
KeyEventAndroid.KEYCODE_VOLUME_MUTE -> {
if (event.action == KeyEventAndroid.ACTION_DOWN) {
volumeController.toggleMute(true, AudioManager.STREAM_SYSTEM)
}
return true
}
else -> {
return false
}
}
}
private fun tryHandlePowerKeyEvent(event: KeyEventAndroid): Boolean {
if (event.keyCode == KeyEventAndroid.KEYCODE_POWER) {
// Perform power dialog action when action is up
if (event.action == KeyEventAndroid.ACTION_UP) {
performGlobalAction(GLOBAL_ACTION_POWER_DIALOG);
}
return true
}
return false
}
private fun insertAccessibilityNode(list: LinkedList<AccessibilityNodeInfo>, node: AccessibilityNodeInfo) { private fun insertAccessibilityNode(list: LinkedList<AccessibilityNodeInfo>, node: AccessibilityNodeInfo) {
if (node == null) { if (node == null) {
return return
@@ -422,7 +475,7 @@ class InputService : AccessibilityService() {
return linkedList return linkedList
} }
private fun trySendKeyEvent(event: android.view.KeyEvent, node: AccessibilityNodeInfo, textToCommit: String?): Boolean { private fun trySendKeyEvent(event: KeyEventAndroid, node: AccessibilityNodeInfo, textToCommit: String?): Boolean {
node.refresh() node.refresh()
this.fakeEditTextForTextStateCalculation?.setSelection(0,0) this.fakeEditTextForTextStateCalculation?.setSelection(0,0)
this.fakeEditTextForTextStateCalculation?.setText(null) this.fakeEditTextForTextStateCalculation?.setText(null)
@@ -487,10 +540,10 @@ class InputService : AccessibilityService() {
it.layout(rect.left, rect.top, rect.right, rect.bottom) it.layout(rect.left, rect.top, rect.right, rect.bottom)
it.onPreDraw() it.onPreDraw()
if (event.action == android.view.KeyEvent.ACTION_DOWN) { if (event.action == KeyEventAndroid.ACTION_DOWN) {
val succ = it.onKeyDown(event.getKeyCode(), event) val succ = it.onKeyDown(event.getKeyCode(), event)
Log.d(logTag, "onKeyDown $succ") Log.d(logTag, "onKeyDown $succ")
} else if (event.action == android.view.KeyEvent.ACTION_UP) { } else if (event.action == KeyEventAndroid.ACTION_UP) {
val success = it.onKeyUp(event.getKeyCode(), event) val success = it.onKeyUp(event.getKeyCode(), event)
Log.d(logTag, "keyup $success") Log.d(logTag, "keyup $success")
} else {} } else {}

View File

@@ -37,6 +37,8 @@ object KeyEventConverter {
action = KeyEvent.ACTION_UP action = KeyEvent.ACTION_UP
} }
// FIXME: The last parameter is the repeat count, not modifiers ?
// https://developer.android.com/reference/android/view/KeyEvent#KeyEvent(long,%20long,%20int,%20int,%20int)
return KeyEvent(0, 0, action, chrValue, 0, modifiers) return KeyEvent(0, 0, action, chrValue, 0, modifiers)
} }
@@ -112,6 +114,10 @@ object KeyEventConverter {
ControlKey.Delete -> KeyEvent.KEYCODE_FORWARD_DEL ControlKey.Delete -> KeyEvent.KEYCODE_FORWARD_DEL
ControlKey.Clear -> KeyEvent.KEYCODE_CLEAR ControlKey.Clear -> KeyEvent.KEYCODE_CLEAR
ControlKey.Pause -> KeyEvent.KEYCODE_BREAK ControlKey.Pause -> KeyEvent.KEYCODE_BREAK
ControlKey.VolumeMute -> KeyEvent.KEYCODE_VOLUME_MUTE
ControlKey.VolumeUp -> KeyEvent.KEYCODE_VOLUME_UP
ControlKey.VolumeDown -> KeyEvent.KEYCODE_VOLUME_DOWN
ControlKey.Power -> KeyEvent.KEYCODE_POWER
else -> 0 // Default to unknown. else -> 0 // Default to unknown.
} }
} }

View File

@@ -7,6 +7,8 @@ package com.carriez.flutter_hbb
* Inspired by [droidVNC-NG] https://github.com/bk138/droidVNC-NG * Inspired by [droidVNC-NG] https://github.com/bk138/droidVNC-NG
*/ */
import ffi.FFI
import android.content.ComponentName import android.content.ComponentName
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
@@ -15,10 +17,20 @@ import android.os.Build
import android.os.IBinder import android.os.IBinder
import android.util.Log import android.util.Log
import android.view.WindowManager import android.view.WindowManager
import android.media.MediaCodecInfo
import android.media.MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface
import android.media.MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar
import android.media.MediaCodecList
import android.media.MediaFormat
import android.util.DisplayMetrics
import androidx.annotation.RequiresApi
import org.json.JSONArray
import org.json.JSONObject
import com.hjq.permissions.XXPermissions import com.hjq.permissions.XXPermissions
import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel
import kotlin.concurrent.thread
class MainActivity : FlutterActivity() { class MainActivity : FlutterActivity() {
@@ -30,6 +42,9 @@ class MainActivity : FlutterActivity() {
private val logTag = "mMainActivity" private val logTag = "mMainActivity"
private var mainService: MainService? = null private var mainService: MainService? = null
private var isAudioStart = false
private val audioRecordHandle = AudioRecordHandle(this, { false }, { isAudioStart })
override fun configureFlutterEngine(flutterEngine: FlutterEngine) { override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine) super.configureFlutterEngine(flutterEngine)
if (MainService.isReady) { if (MainService.isReady) {
@@ -42,6 +57,7 @@ class MainActivity : FlutterActivity() {
channelTag channelTag
) )
initFlutterChannel(flutterMethodChannel!!) initFlutterChannel(flutterMethodChannel!!)
thread { setCodecInfo() }
} }
override fun onResume() { override fun onResume() {
@@ -217,10 +233,159 @@ class MainActivity : FlutterActivity() {
result.success(false) result.success(false)
} }
} }
GET_VALUE -> {
if (call.arguments is String) {
if (call.arguments == KEY_IS_SUPPORT_VOICE_CALL) {
result.success(isSupportVoiceCall())
} else {
result.error("-1", "No such key", null)
}
} else {
result.success(null)
}
}
"on_voice_call_started" -> {
onVoiceCallStarted()
}
"on_voice_call_closed" -> {
onVoiceCallClosed()
}
else -> { else -> {
result.error("-1", "No such method", null) result.error("-1", "No such method", null)
} }
} }
} }
} }
private fun setCodecInfo() {
val codecList = MediaCodecList(MediaCodecList.REGULAR_CODECS)
val codecs = codecList.codecInfos
val codecArray = JSONArray()
val windowManager = getSystemService(Context.WINDOW_SERVICE) as WindowManager
val wh = getScreenSize(windowManager)
var w = wh.first
var h = wh.second
val align = 64
w = (w + align - 1) / align * align
h = (h + align - 1) / align * align
codecs.forEach { codec ->
val codecObject = JSONObject()
codecObject.put("name", codec.name)
codecObject.put("is_encoder", codec.isEncoder)
var hw: Boolean? = null;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
hw = codec.isHardwareAccelerated
} else {
// https://chromium.googlesource.com/external/webrtc/+/HEAD/sdk/android/src/java/org/webrtc/MediaCodecUtils.java#29
// https://chromium.googlesource.com/external/webrtc/+/master/sdk/android/api/org/webrtc/HardwareVideoEncoderFactory.java#229
if (listOf("OMX.google.", "OMX.SEC.", "c2.android").any { codec.name.startsWith(it, true) }) {
hw = false
} else if (listOf("c2.qti", "OMX.qcom.video", "OMX.Exynos", "OMX.hisi", "OMX.MTK", "OMX.Intel", "OMX.Nvidia").any { codec.name.startsWith(it, true) }) {
hw = true
}
}
if (hw != true) {
return@forEach
}
codecObject.put("hw", hw)
var mime_type = ""
codec.supportedTypes.forEach { type ->
if (listOf("video/avc", "video/hevc").contains(type)) { // "video/x-vnd.on2.vp8", "video/x-vnd.on2.vp9", "video/av01"
mime_type = type;
}
}
if (mime_type.isNotEmpty()) {
codecObject.put("mime_type", mime_type)
val caps = codec.getCapabilitiesForType(mime_type)
if (codec.isEncoder) {
// Encoders max_height and max_width are interchangeable
if (!caps.videoCapabilities.isSizeSupported(w,h) && !caps.videoCapabilities.isSizeSupported(h,w)) {
return@forEach
}
}
codecObject.put("min_width", caps.videoCapabilities.supportedWidths.lower)
codecObject.put("max_width", caps.videoCapabilities.supportedWidths.upper)
codecObject.put("min_height", caps.videoCapabilities.supportedHeights.lower)
codecObject.put("max_height", caps.videoCapabilities.supportedHeights.upper)
val surface = caps.colorFormats.contains(COLOR_FormatSurface);
codecObject.put("surface", surface)
val nv12 = caps.colorFormats.contains(COLOR_FormatYUV420SemiPlanar)
codecObject.put("nv12", nv12)
if (!(nv12 || surface)) {
return@forEach
}
codecObject.put("min_bitrate", caps.videoCapabilities.bitrateRange.lower / 1000)
codecObject.put("max_bitrate", caps.videoCapabilities.bitrateRange.upper / 1000)
if (!codec.isEncoder) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
codecObject.put("low_latency", caps.isFeatureSupported(MediaCodecInfo.CodecCapabilities.FEATURE_LowLatency))
}
}
if (!codec.isEncoder) {
return@forEach
}
codecArray.put(codecObject)
}
}
val result = JSONObject()
result.put("version", Build.VERSION.SDK_INT)
result.put("w", w)
result.put("h", h)
result.put("codecs", codecArray)
FFI.setCodecInfo(result.toString())
}
private fun onVoiceCallStarted() {
var ok = false
mainService?.let {
ok = it.onVoiceCallStarted()
} ?: let {
isAudioStart = true
ok = audioRecordHandle.onVoiceCallStarted(null)
}
if (!ok) {
// Rarely happens, So we just add log and msgbox here.
Log.e(logTag, "onVoiceCallStarted fail")
flutterMethodChannel?.invokeMethod("msgbox", mapOf(
"type" to "custom-nook-nocancel-hasclose-error",
"title" to "Voice call",
"text" to "Failed to start voice call."))
} else {
Log.d(logTag, "onVoiceCallStarted success")
}
}
private fun onVoiceCallClosed() {
var ok = false
mainService?.let {
ok = it.onVoiceCallClosed()
} ?: let {
isAudioStart = false
ok = audioRecordHandle.onVoiceCallClosed(null)
}
if (!ok) {
// Rarely happens, So we just add log and msgbox here.
Log.e(logTag, "onVoiceCallClosed fail")
flutterMethodChannel?.invokeMethod("msgbox", mapOf(
"type" to "custom-nook-nocancel-hasclose-error",
"title" to "Voice call",
"text" to "Failed to stop voice call."))
} else {
Log.d(logTag, "onVoiceCallClosed success")
}
}
override fun onStop() {
super.onStop()
val disableFloatingWindow = FFI.getLocalOption("disable-floating-window") == "Y"
if (!disableFloatingWindow && MainService.isReady) {
startService(Intent(this, FloatingWindowService::class.java))
}
}
override fun onStart() {
super.onStart()
stopService(Intent(this, FloatingWindowService::class.java))
}
} }

View File

@@ -1,5 +1,7 @@
package com.carriez.flutter_hbb package com.carriez.flutter_hbb
import ffi.FFI
/** /**
* Capture screen,get video and audio,send to rust. * Capture screen,get video and audio,send to rust.
* Dispatch notifications * Dispatch notifications
@@ -52,27 +54,19 @@ const val NOTIFY_ID_OFFSET = 100
const val MIME_TYPE = MediaFormat.MIMETYPE_VIDEO_VP9 const val MIME_TYPE = MediaFormat.MIMETYPE_VIDEO_VP9
// video const // video const
const val MAX_SCREEN_SIZE = 1200 const val MAX_SCREEN_SIZE = 1200
const val VIDEO_KEY_BIT_RATE = 1024_000 const val VIDEO_KEY_BIT_RATE = 1024_000
const val VIDEO_KEY_FRAME_RATE = 30 const val VIDEO_KEY_FRAME_RATE = 30
// audio const
const val AUDIO_ENCODING = AudioFormat.ENCODING_PCM_FLOAT // ENCODING_OPUS need API 30
const val AUDIO_SAMPLE_RATE = 48000
const val AUDIO_CHANNEL_MASK = AudioFormat.CHANNEL_IN_STEREO
class MainService : Service() { class MainService : Service() {
init {
System.loadLibrary("rustdesk")
}
@Keep @Keep
@RequiresApi(Build.VERSION_CODES.N) @RequiresApi(Build.VERSION_CODES.N)
fun rustPointerInput(kind: String, mask: Int, x: Int, y: Int) { fun rustPointerInput(kind: Int, mask: Int, x: Int, y: Int) {
// turn on screen with LIFT_DOWN when screen off // turn on screen with LIFT_DOWN when screen off
if (!powerManager.isInteractive && (kind == "touch" || mask == LIFT_DOWN)) { if (!powerManager.isInteractive && (kind == 0 || mask == LIFT_DOWN)) {
if (wakeLock.isHeld) { if (wakeLock.isHeld) {
Log.d(logTag, "Turn on Screen, WakeLock release") Log.d(logTag, "Turn on Screen, WakeLock release")
wakeLock.release() wakeLock.release()
@@ -81,10 +75,10 @@ class MainService : Service() {
wakeLock.acquire(5000) wakeLock.acquire(5000)
} else { } else {
when (kind) { when (kind) {
"touch" -> { 0 -> { // touch
InputService.ctx?.onTouchInput(mask, x, y) InputService.ctx?.onTouchInput(mask, x, y)
} }
"mouse" -> { 1 -> { // mouse
InputService.ctx?.onMouseInput(mask, x, y) InputService.ctx?.onMouseInput(mask, x, y)
} }
else -> { else -> {
@@ -109,6 +103,9 @@ class MainService : Service() {
put("scale",SCREEN_INFO.scale) put("scale",SCREEN_INFO.scale)
}.toString() }.toString()
} }
"is_start" -> {
isStart.toString()
}
else -> "" else -> ""
} }
} }
@@ -141,10 +138,51 @@ class MainService : Service() {
e.printStackTrace() e.printStackTrace()
} }
} }
"update_voice_call_state" -> {
try {
val jsonObject = JSONObject(arg1)
val id = jsonObject["id"] as Int
val username = jsonObject["name"] as String
val peerId = jsonObject["peer_id"] as String
val inVoiceCall = jsonObject["in_voice_call"] as Boolean
val incomingVoiceCall = jsonObject["incoming_voice_call"] as Boolean
if (!inVoiceCall) {
if (incomingVoiceCall) {
voiceCallRequestNotification(id, "Voice Call Request", username, peerId)
} else {
if (!audioRecordHandle.switchOutVoiceCall(mediaProjection)) {
Log.e(logTag, "switchOutVoiceCall fail")
MainActivity.flutterMethodChannel?.invokeMethod("msgbox", mapOf(
"type" to "custom-nook-nocancel-hasclose-error",
"title" to "Voice call",
"text" to "Failed to switch out voice call."))
}
}
} else {
if (!audioRecordHandle.switchToVoiceCall(mediaProjection)) {
Log.e(logTag, "switchToVoiceCall fail")
MainActivity.flutterMethodChannel?.invokeMethod("msgbox", mapOf(
"type" to "custom-nook-nocancel-hasclose-error",
"title" to "Voice call",
"text" to "Failed to switch to voice call."))
}
}
} catch (e: JSONException) {
e.printStackTrace()
}
}
"stop_capture" -> { "stop_capture" -> {
Log.d(logTag, "from rust:stop_capture") Log.d(logTag, "from rust:stop_capture")
stopCapture() stopCapture()
} }
"half_scale" -> {
val halfScale = arg1.toBoolean()
if (isHalfScale != halfScale) {
isHalfScale = halfScale
updateScreenInfo(resources.configuration.orientation)
}
}
else -> { else -> {
} }
} }
@@ -156,38 +194,23 @@ class MainService : Service() {
private val powerManager: PowerManager by lazy { applicationContext.getSystemService(Context.POWER_SERVICE) as PowerManager } private val powerManager: PowerManager by lazy { applicationContext.getSystemService(Context.POWER_SERVICE) as PowerManager }
private val wakeLock: PowerManager.WakeLock by lazy { powerManager.newWakeLock(PowerManager.ACQUIRE_CAUSES_WAKEUP or PowerManager.SCREEN_BRIGHT_WAKE_LOCK, "rustdesk:wakelock")} private val wakeLock: PowerManager.WakeLock by lazy { powerManager.newWakeLock(PowerManager.ACQUIRE_CAUSES_WAKEUP or PowerManager.SCREEN_BRIGHT_WAKE_LOCK, "rustdesk:wakelock")}
// jvm call rust
private external fun init(ctx: Context)
/// When app start on boot, app_dir will not be passed from flutter
/// so pass a app_dir here to rust server
private external fun startServer(app_dir: String)
private external fun startService()
private external fun onVideoFrameUpdate(buf: ByteBuffer)
private external fun onAudioFrameUpdate(buf: ByteBuffer)
private external fun translateLocale(localeName: String, input: String): String
private external fun refreshScreen()
private external fun setFrameRawEnable(name: String, value: Boolean)
// private external fun sendVp9(data: ByteArray)
private fun translate(input: String): String {
Log.d(logTag, "translate:$LOCAL_NAME")
return translateLocale(LOCAL_NAME, input)
}
companion object { companion object {
private var _isReady = false // media permission ready status private var _isReady = false // media permission ready status
private var _isStart = false // screen capture start status private var _isStart = false // screen capture start status
private var _isAudioStart = false // audio capture start status
val isReady: Boolean val isReady: Boolean
get() = _isReady get() = _isReady
val isStart: Boolean val isStart: Boolean
get() = _isStart get() = _isStart
val isAudioStart: Boolean
get() = _isAudioStart
} }
private val logTag = "LOG_SERVICE" private val logTag = "LOG_SERVICE"
private val useVP9 = false private val useVP9 = false
private val binder = LocalBinder() private val binder = LocalBinder()
private var reuseVirtualDisplay = Build.VERSION.SDK_INT > 33
// video // video
private var mediaProjection: MediaProjection? = null private var mediaProjection: MediaProjection? = null
@@ -198,10 +221,7 @@ class MainService : Service() {
private var virtualDisplay: VirtualDisplay? = null private var virtualDisplay: VirtualDisplay? = null
// audio // audio
private var audioRecorder: AudioRecord? = null private val audioRecordHandle = AudioRecordHandle(this, { isStart }, { isAudioStart })
private var audioReader: AudioReader? = null
private var minBufferSize = 0
private var audioRecordStat = false
// notification // notification
private lateinit var notificationManager: NotificationManager private lateinit var notificationManager: NotificationManager
@@ -210,8 +230,8 @@ class MainService : Service() {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
Log.d(logTag,"MainService onCreate") Log.d(logTag,"MainService onCreate, sdk int:${Build.VERSION.SDK_INT} reuseVirtualDisplay:$reuseVirtualDisplay")
init(this) FFI.init(this)
HandlerThread("Service", Process.THREAD_PRIORITY_BACKGROUND).apply { HandlerThread("Service", Process.THREAD_PRIORITY_BACKGROUND).apply {
start() start()
serviceLooper = looper serviceLooper = looper
@@ -223,16 +243,18 @@ 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, "") ?: ""
startServer(configPath) FFI.startServer(configPath, "")
createForegroundNotification() createForegroundNotification()
} }
override fun onDestroy() { override fun onDestroy() {
checkMediaPermission() checkMediaPermission()
stopService(Intent(this, FloatingWindowService::class.java))
super.onDestroy() super.onDestroy()
} }
private var isHalfScale: Boolean? = null;
private fun updateScreenInfo(orientation: Int) { private fun updateScreenInfo(orientation: Int) {
var w: Int var w: Int
var h: Int var h: Int
@@ -265,7 +287,7 @@ class MainService : Service() {
Log.d(logTag,"updateScreenInfo:w:$w,h:$h") Log.d(logTag,"updateScreenInfo:w:$w,h:$h")
var scale = 1 var scale = 1
if (w != 0 && h != 0) { if (w != 0 && h != 0) {
if (w > MAX_SCREEN_SIZE || h > MAX_SCREEN_SIZE) { if (isHalfScale == true && (w > MAX_SCREEN_SIZE || h > MAX_SCREEN_SIZE)) {
scale = 2 scale = 2
w /= scale w /= scale
h /= scale h /= scale
@@ -278,7 +300,7 @@ class MainService : Service() {
SCREEN_INFO.dpi = dpi SCREEN_INFO.dpi = dpi
if (isStart) { if (isStart) {
stopCapture() stopCapture()
refreshScreen() FFI.refreshScreen()
startCapture() startCapture()
} }
} }
@@ -306,7 +328,7 @@ class MainService : Service() {
createForegroundNotification() createForegroundNotification()
if (intent.getBooleanExtra(EXT_INIT_FROM_BOOT, false)) { if (intent.getBooleanExtra(EXT_INIT_FROM_BOOT, false)) {
startService() FFI.startService()
} }
Log.d(logTag, "service starting: ${startId}:${Thread.currentThread()}") Log.d(logTag, "service starting: ${startId}:${Thread.currentThread()}")
val mediaProjectionManager = val mediaProjectionManager =
@@ -354,12 +376,13 @@ class MainService : Service() {
).apply { ).apply {
setOnImageAvailableListener({ imageReader: ImageReader -> setOnImageAvailableListener({ imageReader: ImageReader ->
try { try {
// If not call acquireLatestImage, listener will not be called again
imageReader.acquireLatestImage().use { image -> imageReader.acquireLatestImage().use { image ->
if (image == null) return@setOnImageAvailableListener if (image == null || !isStart) return@setOnImageAvailableListener
val planes = image.planes val planes = image.planes
val buffer = planes[0].buffer val buffer = planes[0].buffer
buffer.rewind() buffer.rewind()
onVideoFrameUpdate(buffer) FFI.onVideoFrameUpdate(buffer)
} }
} catch (ignored: java.lang.Exception) { } catch (ignored: java.lang.Exception) {
} }
@@ -370,6 +393,14 @@ class MainService : Service() {
} }
} }
fun onVoiceCallStarted(): Boolean {
return audioRecordHandle.onVoiceCallStarted(mediaProjection)
}
fun onVoiceCallClosed(): Boolean {
return audioRecordHandle.onVoiceCallClosed(mediaProjection)
}
fun startCapture(): Boolean { fun startCapture(): Boolean {
if (isStart) { if (isStart) {
return true return true
@@ -378,6 +409,7 @@ class MainService : Service() {
Log.w(logTag, "startCapture fail,mediaProjection is null") Log.w(logTag, "startCapture fail,mediaProjection is null")
return false return false
} }
updateScreenInfo(resources.configuration.orientation) updateScreenInfo(resources.configuration.orientation)
Log.d(logTag, "Start Capture") Log.d(logTag, "Start Capture")
surface = createSurface() surface = createSurface()
@@ -389,51 +421,71 @@ class MainService : Service() {
} }
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
startAudioRecorder() if (!audioRecordHandle.createAudioRecorder(false, mediaProjection)) {
Log.d(logTag, "createAudioRecorder fail")
} else {
Log.d(logTag, "audio recorder start")
audioRecordHandle.startAudioRecorder()
}
} }
checkMediaPermission() checkMediaPermission()
_isStart = true _isStart = true
setFrameRawEnable("video",true) FFI.setFrameRawEnable("video",true)
setFrameRawEnable("audio",true)
return true return true
} }
@Synchronized @Synchronized
fun stopCapture() { fun stopCapture() {
Log.d(logTag, "Stop Capture") Log.d(logTag, "Stop Capture")
setFrameRawEnable("video",false) FFI.setFrameRawEnable("video",false)
setFrameRawEnable("audio",false)
_isStart = false _isStart = false
// release video // release video
virtualDisplay?.release() if (reuseVirtualDisplay) {
surface?.release() // The virtual display video projection can be paused by calling `setSurface(null)`.
// https://developer.android.com/reference/android/hardware/display/VirtualDisplay.Callback
// https://learn.microsoft.com/en-us/dotnet/api/android.hardware.display.virtualdisplay.callback.onpaused?view=net-android-34.0
virtualDisplay?.setSurface(null)
} else {
virtualDisplay?.release()
}
// suface needs to be release after `imageReader.close()` to imageReader access released surface
// https://github.com/rustdesk/rustdesk/issues/4118#issuecomment-1515666629
imageReader?.close() imageReader?.close()
imageReader = null
videoEncoder?.let { videoEncoder?.let {
it.signalEndOfInputStream() it.signalEndOfInputStream()
it.stop() it.stop()
it.release() it.release()
} }
virtualDisplay = null if (!reuseVirtualDisplay) {
virtualDisplay = null
}
videoEncoder = null videoEncoder = null
// suface needs to be release after `imageReader.close()` to imageReader access released surface
// https://github.com/rustdesk/rustdesk/issues/4118#issuecomment-1515666629
surface?.release()
// release audio // release audio
audioRecordStat = false _isAudioStart = false
audioRecorder?.release() audioRecordHandle.tryReleaseAudio()
audioRecorder = null
minBufferSize = 0
} }
fun destroy() { fun destroy() {
Log.d(logTag, "destroy service") Log.d(logTag, "destroy service")
_isReady = false _isReady = false
_isAudioStart = false
stopCapture() stopCapture()
imageReader?.close()
imageReader = null if (reuseVirtualDisplay) {
virtualDisplay?.release()
virtualDisplay = null
}
mediaProjection = null mediaProjection = null
checkMediaPermission() checkMediaPermission()
stopForeground(true) stopForeground(true)
stopService(Intent(this, FloatingWindowService::class.java))
stopSelf() stopSelf()
} }
@@ -459,11 +511,7 @@ class MainService : Service() {
Log.d(logTag, "startRawVideoRecorder failed,surface is null") Log.d(logTag, "startRawVideoRecorder failed,surface is null")
return return
} }
virtualDisplay = mp.createVirtualDisplay( createOrSetVirtualDisplay(mp, surface!!)
"RustDeskVD",
SCREEN_INFO.width, SCREEN_INFO.height, SCREEN_INFO.dpi, VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
surface, null, null
)
} }
private fun startVP9VideoRecorder(mp: MediaProjection) { private fun startVP9VideoRecorder(mp: MediaProjection) {
@@ -475,11 +523,28 @@ class MainService : Service() {
} }
it.setCallback(cb) it.setCallback(cb)
it.start() it.start()
virtualDisplay = mp.createVirtualDisplay( createOrSetVirtualDisplay(mp, surface!!)
"RustDeskVD", }
SCREEN_INFO.width, SCREEN_INFO.height, SCREEN_INFO.dpi, VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR, }
surface, null, null
) // 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.
private fun createOrSetVirtualDisplay(mp: MediaProjection, s: Surface) {
try {
virtualDisplay?.let {
it.resize(SCREEN_INFO.width, SCREEN_INFO.height, SCREEN_INFO.dpi)
it.setSurface(s)
} ?: let {
virtualDisplay = mp.createVirtualDisplay(
"RustDeskVD",
SCREEN_INFO.width, SCREEN_INFO.height, SCREEN_INFO.dpi, VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
s, null, null
)
}
} catch (e: SecurityException) {
Log.w(logTag, "createOrSetVirtualDisplay: got SecurityException, re-requesting confirmation");
// This initiates a prompt dialog for the user to confirm screen projection.
requestMediaProjection()
} }
} }
@@ -507,7 +572,6 @@ class MainService : Service() {
} }
} }
private fun createMediaCodec() { private fun createMediaCodec() {
Log.d(logTag, "MediaFormat.MIMETYPE_VIDEO_VP9 :$MIME_TYPE") Log.d(logTag, "MediaFormat.MIMETYPE_VIDEO_VP9 :$MIME_TYPE")
videoEncoder = MediaCodec.createEncoderByType(MIME_TYPE) videoEncoder = MediaCodec.createEncoderByType(MIME_TYPE)
@@ -527,76 +591,6 @@ class MainService : Service() {
} }
} }
@RequiresApi(Build.VERSION_CODES.M)
private fun startAudioRecorder() {
checkAudioRecorder()
if (audioReader != null && audioRecorder != null && minBufferSize != 0) {
try {
audioRecorder!!.startRecording()
audioRecordStat = true
thread {
while (audioRecordStat) {
audioReader!!.readSync(audioRecorder!!)?.let {
onAudioFrameUpdate(it)
}
}
Log.d(logTag, "Exit audio thread")
}
} catch (e: Exception) {
Log.d(logTag, "startAudioRecorder fail:$e")
}
} else {
Log.d(logTag, "startAudioRecorder fail")
}
}
@RequiresApi(Build.VERSION_CODES.M)
private fun checkAudioRecorder() {
if (audioRecorder != null && audioRecorder != null && minBufferSize != 0) {
return
}
// read f32 to byte , length * 4
minBufferSize = 2 * 4 * AudioRecord.getMinBufferSize(
AUDIO_SAMPLE_RATE,
AUDIO_CHANNEL_MASK,
AUDIO_ENCODING
)
if (minBufferSize == 0) {
Log.d(logTag, "get min buffer size fail!")
return
}
audioReader = AudioReader(minBufferSize, 4)
Log.d(logTag, "init audioData len:$minBufferSize")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
mediaProjection?.let {
val apcc = AudioPlaybackCaptureConfiguration.Builder(it)
.addMatchingUsage(AudioAttributes.USAGE_MEDIA)
.addMatchingUsage(AudioAttributes.USAGE_ALARM)
.addMatchingUsage(AudioAttributes.USAGE_GAME)
.addMatchingUsage(AudioAttributes.USAGE_UNKNOWN).build()
if (ActivityCompat.checkSelfPermission(
this,
Manifest.permission.RECORD_AUDIO
) != PackageManager.PERMISSION_GRANTED
) {
return
}
audioRecorder = AudioRecord.Builder()
.setAudioFormat(
AudioFormat.Builder()
.setEncoding(AUDIO_ENCODING)
.setSampleRate(AUDIO_SAMPLE_RATE)
.setChannelMask(AUDIO_CHANNEL_MASK).build()
)
.setAudioPlaybackCaptureConfig(apcc)
.setBufferSizeInBytes(minBufferSize).build()
Log.d(logTag, "createAudioRecorder done,minBufferSize:$minBufferSize")
return
}
}
Log.d(logTag, "createAudioRecorder fail")
}
private fun initNotification() { private fun initNotification() {
notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationChannel = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { notificationChannel = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
@@ -681,6 +675,21 @@ class MainService : Service() {
notificationManager.notify(getClientNotifyID(clientID), notification) notificationManager.notify(getClientNotifyID(clientID), notification)
} }
private fun voiceCallRequestNotification(
clientID: Int,
type: String,
username: String,
peerId: String
) {
val notification = notificationBuilder
.setOngoing(false)
.setPriority(NotificationCompat.PRIORITY_MAX)
.setContentTitle(translate("Do you accept?"))
.setContentText("$type:$username-$peerId")
.build()
notificationManager.notify(getClientNotifyID(clientID), notification)
}
private fun getClientNotifyID(clientID: Int): Int { private fun getClientNotifyID(clientID: Int): Int {
return clientID + NOTIFY_ID_OFFSET return clientID + NOTIFY_ID_OFFSET
} }

View File

@@ -0,0 +1,78 @@
package com.carriez.flutter_hbb
// Inspired by https://github.com/yosemiteyss/flutter_volume_controller/blob/main/android/src/main/kotlin/com/yosemiteyss/flutter_volume_controller/VolumeController.kt
import android.media.AudioManager
import android.os.Build
import android.util.Log
class VolumeController(private val audioManager: AudioManager) {
private val logTag = "volume controller"
fun getVolume(streamType: Int): Double {
val current = audioManager.getStreamVolume(streamType)
val max = audioManager.getStreamMaxVolume(streamType)
return current.toDouble() / max
}
fun setVolume(volume: Double, showSystemUI: Boolean, streamType: Int) {
val max = audioManager.getStreamMaxVolume(streamType)
audioManager.setStreamVolume(
streamType,
(max * volume).toInt(),
if (showSystemUI) AudioManager.FLAG_SHOW_UI else 0
)
}
fun raiseVolume(step: Double?, showSystemUI: Boolean, streamType: Int) {
if (step == null) {
audioManager.adjustStreamVolume(
streamType,
AudioManager.ADJUST_RAISE,
if (showSystemUI) AudioManager.FLAG_SHOW_UI else 0
)
} else {
val target = getVolume(streamType) + step
setVolume(target, showSystemUI, streamType)
}
}
fun lowerVolume(step: Double?, showSystemUI: Boolean, streamType: Int) {
if (step == null) {
audioManager.adjustStreamVolume(
streamType,
AudioManager.ADJUST_LOWER,
if (showSystemUI) AudioManager.FLAG_SHOW_UI else 0
)
} else {
val target = getVolume(streamType) - step
setVolume(target, showSystemUI, streamType)
}
}
fun getMute(streamType: Int): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
audioManager.isStreamMute(streamType)
} else {
audioManager.getStreamVolume(streamType) == 0
}
}
private fun setMute(isMuted: Boolean, showSystemUI: Boolean, streamType: Int) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
audioManager.adjustStreamVolume(
streamType,
if (isMuted) AudioManager.ADJUST_MUTE else AudioManager.ADJUST_UNMUTE,
if (showSystemUI) AudioManager.FLAG_SHOW_UI else 0
)
} else {
audioManager.setStreamMute(streamType, isMuted)
}
}
fun toggleMute(showSystemUI: Boolean, streamType: Int) {
val isMuted = getMute(streamType)
setMute(!isMuted, showSystemUI, streamType)
}
}

View File

@@ -15,10 +15,14 @@ import android.os.Looper
import android.os.PowerManager import android.os.PowerManager
import android.provider.Settings import android.provider.Settings
import android.provider.Settings.* import android.provider.Settings.*
import android.util.DisplayMetrics
import android.util.Log
import android.view.WindowManager
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.core.content.ContextCompat.getSystemService import androidx.core.content.ContextCompat.getSystemService
import com.hjq.permissions.Permission import com.hjq.permissions.Permission
import com.hjq.permissions.XXPermissions import com.hjq.permissions.XXPermissions
import ffi.FFI
import java.nio.ByteBuffer import java.nio.ByteBuffer
import java.util.* import java.util.*
@@ -43,6 +47,9 @@ const val START_ACTION = "start_action"
const val GET_START_ON_BOOT_OPT = "get_start_on_boot_opt" const val GET_START_ON_BOOT_OPT = "get_start_on_boot_opt"
const val SET_START_ON_BOOT_OPT = "set_start_on_boot_opt" const val SET_START_ON_BOOT_OPT = "set_start_on_boot_opt"
const val SYNC_APP_DIR_CONFIG_PATH = "sync_app_dir" const val SYNC_APP_DIR_CONFIG_PATH = "sync_app_dir"
const val GET_VALUE = "get_value"
const val KEY_IS_SUPPORT_VOICE_CALL = "KEY_IS_SUPPORT_VOICE_CALL"
const val KEY_SHARED_PREFERENCES = "KEY_SHARED_PREFERENCES" const val KEY_SHARED_PREFERENCES = "KEY_SHARED_PREFERENCES"
const val KEY_START_ON_BOOT_OPT = "KEY_START_ON_BOOT_OPT" const val KEY_START_ON_BOOT_OPT = "KEY_START_ON_BOOT_OPT"
@@ -56,6 +63,11 @@ data class Info(
var width: Int, var height: Int, var scale: Int, var dpi: Int var width: Int, var height: Int, var scale: Int, var dpi: Int
) )
fun isSupportVoiceCall(): Boolean {
// https://developer.android.com/reference/android/media/MediaRecorder.AudioSource#VOICE_COMMUNICATION
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
}
fun requestPermission(context: Context, type: String) { fun requestPermission(context: Context, type: String) {
XXPermissions.with(context) XXPermissions.with(context)
.permission(type) .permission(type)
@@ -120,3 +132,26 @@ class AudioReader(val bufSize: Int, private val maxFrames: Int) {
} }
} }
} }
fun getScreenSize(windowManager: WindowManager) : Pair<Int, Int>{
var w = 0
var h = 0
@Suppress("DEPRECATION")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
val m = windowManager.maximumWindowMetrics
w = m.bounds.width()
h = m.bounds.height()
} else {
val dm = DisplayMetrics()
windowManager.defaultDisplay.getRealMetrics(dm)
w = dm.widthPixels
h = dm.heightPixels
}
return Pair(w, h)
}
fun translate(input: String): String {
Log.d("common", "translate:$LOCAL_NAME")
return FFI.translateLocale(LOCAL_NAME, input)
}

View File

@@ -0,0 +1,23 @@
// ffi.kt
package ffi
import android.content.Context
import java.nio.ByteBuffer
object FFI {
init {
System.loadLibrary("rustdesk")
}
external fun init(ctx: Context)
external fun startServer(app_dir: String, custom_client_config: String)
external fun startService()
external fun onVideoFrameUpdate(buf: ByteBuffer)
external fun onAudioFrameUpdate(buf: ByteBuffer)
external fun translateLocale(localeName: String, input: String): String
external fun refreshScreen()
external fun setFrameRawEnable(name: String, value: Boolean)
external fun setCodecInfo(info: String)
external fun getLocalOption(key: String): String
}

View File

@@ -0,0 +1,7 @@
<vector xmlns:aapt="http://schemas.android.com/aapt" xmlns:android="http://schemas.android.com/apk/res/android" android:height="320dp" android:viewportHeight="32" android:viewportWidth="32" android:width="320dp">
<path android:fillColor="#ffffff" android:pathData="M16,0L16,0A16,16 0,0 1,32 16L32,16A16,16 0,0 1,16 32L16,32A16,16 0,0 1,0 16L0,16A16,16 0,0 1,16 0z" android:strokeColor="#00000000" android:strokeWidth="1"/>
<path android:fillColor="#1a1a1a" android:pathData="m23.89,10.135 l-1.807,1.795c-0.318,0.285 -0.472,0.744 -0.293,1.131 1.204,2.518 0.747,5.52 -1.228,7.494 -1.976,1.973 -4.981,2.429 -7.502,1.226 -0.371,-0.166 -0.807,-0.025 -1.093,0.265l-1.836,1.833c-0.216,0.211 -0.322,0.51 -0.288,0.809 0.034,0.3 0.206,0.567 0.463,0.723 4.326,2.618 9.882,1.951 13.463,-1.618 3.581,-3.568 4.264,-9.115 1.655,-13.443 -0.15,-0.263 -0.414,-0.442 -0.714,-0.484 -0.3,-0.043 -0.603,0.058 -0.819,0.269zM8.265,8.184c-3.599,3.554 -4.304,9.103 -1.709,13.441 0.15,0.264 0.413,0.443 0.714,0.485 0.3,0.042 0.603,-0.058 0.82,-0.27l1.797,-1.785c0.325,-0.285 0.484,-0.749 0.303,-1.141 -1.204,-2.518 -0.748,-5.52 1.228,-7.493 1.975,-1.973 4.981,-2.429 7.502,-1.227 0.367,0.165 0.797,0.028 1.084,-0.254l1.846,-1.844c0.216,-0.211 0.322,-0.509 0.288,-0.809 -0.035,-0.299 -0.206,-0.566 -0.463,-0.723 -4.334,-2.596 -9.881,-1.908 -13.448,1.668z" android:strokeWidth="0.987992"/>
</vector>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="-4 -4 32 32" width="24px" fill="#5f6368"><path d="M0 0h24v24H0z" fill="none"/><path d="M20 2H4c-1.1 0-1.99.9-1.99 2L2 22l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm-2 12H6v-2h12v2zm0-3H6V9h12v2zm0-3H6V6h12v2z"/></svg>

After

Width:  |  Height:  |  Size: 277 B

View File

@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200" class="icon" viewBox="0 0 1024 1024"><path d="M608 160c141.16 0 256 114.84 256 256 0 17.67 14.33 32 32 32s32-14.33 32-32c0-85.48-33.29-165.83-93.73-226.27C773.83 129.29 693.47 96 608 96c-17.67 0-32 14.33-32 32s14.33 32 32 32zm-24 168c61.76 0 112 50.24 112 112 0 17.67 14.33 32 32 32s32-14.33 32-32c0-97.05-78.95-176-176-176-17.67 0-32 14.33-32 32s14.33 32 32 32z"/><path d="M808.3 561.21c-12.76-3.83-25.7-6.2-38.46-7.03-60.3-4.5-116.45 18.9-146.55 61.08-22.6 31.67-45.66 50.01-68.52 54.5-17.71 3.48-33.12-1.7-45.49-5.85-2.66-.9-5.18-1.74-7.68-2.49-93.84-28.17-156.49-108.42-155.9-199.7.16-24.14 16.38-45.98 42.34-56.99 43.75-18.56 77.35-54 92.17-97.22 7.02-20.48 9.65-41.57 7.8-62.68-2.66-31.78-15.1-61.85-35.96-86.96-21.1-25.39-49.51-44-82.16-53.8-4.07-1.22-8.22-2.31-12.35-3.23-30.63-6.87-62.7-4.49-92.73 6.88-29.24 11.07-54.56 29.86-73.23 54.33a476.073 476.073 0 0 0-36.42 55.34 477.675 477.675 0 0 0-17.24 33.81C109.84 312.17 95.73 376.76 96 443.15c.26 63.78 13.7 126.26 39.95 185.7 27.55 62.39 69.3 119.84 120.74 166.11 54.14 48.71 117.6 84.85 188.63 107.4C499.02 919.41 554.33 928 610.21 928c10.99 0 22.01-.33 33.03-1 17.64-1.07 31.08-16.23 30.01-33.87-1.07-17.64-16.22-31.08-33.87-30.01-59.19 3.57-117.96-3.75-174.69-21.76C342.78 802.66 244.31 715.78 194.5 603c-46.76-105.9-46.21-221.33 1.55-325.03 4.55-9.87 9.57-19.72 14.92-29.26 9.29-16.54 19.89-32.64 31.5-47.86 23.47-30.77 64.09-45.87 101.07-37.58 2.66.6 5.33 1.3 7.95 2.08 40.93 12.29 69.48 45.6 72.75 84.86 0 .05.01.1.01.15 1.07 12.15-.47 24.39-4.58 36.37-8.94 26.06-29.58 47.59-56.63 59.07-23.58 10.01-43.63 25.72-57.99 45.45-15.12 20.78-23.2 45-23.36 70.05-.37 57.15 19 114.29 54.53 160.91 36.46 47.83 87.28 82.58 146.96 100.49 1.5.45 3.44 1.1 5.69 1.86 29.79 10.01 108.9 36.59 186.49-72.13 16.95-23.75 52.2-37.26 89.81-34.42l.36.03c7.97.51 16.17 2.02 24.34 4.47 22.12 6.64 42.04 25.38 56.11 52.77 16.97 33.04 21.71 72.53 12.1 100.56l-.16.47c-5.54 16.05-17.78 29.48-34.47 37.8-15.82 7.89-22.24 27.1-14.36 42.92s27.1 22.24 42.92 14.36c31.78-15.85 55.36-42.19 66.41-74.2l.18-.53c15.23-44.4 9.22-102.11-15.68-150.61-22.07-43.02-55.68-73.15-94.62-84.84z"/></svg> <svg xmlns="http://www.w3.org/2000/svg" width="200" height="200" class="icon" viewBox="-186 -186 1365 1365"><path d="M608 160c141.16 0 256 114.84 256 256 0 17.67 14.33 32 32 32s32-14.33 32-32c0-85.48-33.29-165.83-93.73-226.27C773.83 129.29 693.47 96 608 96c-17.67 0-32 14.33-32 32s14.33 32 32 32zm-24 168c61.76 0 112 50.24 112 112 0 17.67 14.33 32 32 32s32-14.33 32-32c0-97.05-78.95-176-176-176-17.67 0-32 14.33-32 32s14.33 32 32 32z"/><path d="M808.3 561.21c-12.76-3.83-25.7-6.2-38.46-7.03-60.3-4.5-116.45 18.9-146.55 61.08-22.6 31.67-45.66 50.01-68.52 54.5-17.71 3.48-33.12-1.7-45.49-5.85-2.66-.9-5.18-1.74-7.68-2.49-93.84-28.17-156.49-108.42-155.9-199.7.16-24.14 16.38-45.98 42.34-56.99 43.75-18.56 77.35-54 92.17-97.22 7.02-20.48 9.65-41.57 7.8-62.68-2.66-31.78-15.1-61.85-35.96-86.96-21.1-25.39-49.51-44-82.16-53.8-4.07-1.22-8.22-2.31-12.35-3.23-30.63-6.87-62.7-4.49-92.73 6.88-29.24 11.07-54.56 29.86-73.23 54.33a476.073 476.073 0 0 0-36.42 55.34 477.675 477.675 0 0 0-17.24 33.81C109.84 312.17 95.73 376.76 96 443.15c.26 63.78 13.7 126.26 39.95 185.7 27.55 62.39 69.3 119.84 120.74 166.11 54.14 48.71 117.6 84.85 188.63 107.4C499.02 919.41 554.33 928 610.21 928c10.99 0 22.01-.33 33.03-1 17.64-1.07 31.08-16.23 30.01-33.87-1.07-17.64-16.22-31.08-33.87-30.01-59.19 3.57-117.96-3.75-174.69-21.76C342.78 802.66 244.31 715.78 194.5 603c-46.76-105.9-46.21-221.33 1.55-325.03 4.55-9.87 9.57-19.72 14.92-29.26 9.29-16.54 19.89-32.64 31.5-47.86 23.47-30.77 64.09-45.87 101.07-37.58 2.66.6 5.33 1.3 7.95 2.08 40.93 12.29 69.48 45.6 72.75 84.86 0 .05.01.1.01.15 1.07 12.15-.47 24.39-4.58 36.37-8.94 26.06-29.58 47.59-56.63 59.07-23.58 10.01-43.63 25.72-57.99 45.45-15.12 20.78-23.2 45-23.36 70.05-.37 57.15 19 114.29 54.53 160.91 36.46 47.83 87.28 82.58 146.96 100.49 1.5.45 3.44 1.1 5.69 1.86 29.79 10.01 108.9 36.59 186.49-72.13 16.95-23.75 52.2-37.26 89.81-34.42l.36.03c7.97.51 16.17 2.02 24.34 4.47 22.12 6.64 42.04 25.38 56.11 52.77 16.97 33.04 21.71 72.53 12.1 100.56l-.16.47c-5.54 16.05-17.78 29.48-34.47 37.8-15.82 7.89-22.24 27.1-14.36 42.92s27.1 22.24 42.92 14.36c31.78-15.85 55.36-42.19 66.41-74.2l.18-.53c15.23-44.4 9.22-102.11-15.68-150.61-22.07-43.02-55.68-73.15-94.62-84.84z"/></svg>

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

564
flutter/build_fdroid.sh Executable file
View File

@@ -0,0 +1,564 @@
#!/bin/bash
set -x
#
# Script to build F-Droid release of RustDesk
#
# Copyright (C) 2024, The RustDesk Authors
# 2024, Vasyl Gello <vasek.gello@gmail.com>
#
# The script is invoked by F-Droid builder system ste-by-step.
#
# It accepts the following arguments:
#
# - versionName from https://github.com/rustdesk/rustdesk/releases/download/fdroid-version/rustdesk-version.txt
# - versionCode from https://github.com/rustdesk/rustdesk/releases/download/fdroid-version/rustdesk-version.txt
# - Android architecture to build APK for: armeabi-v7a arm64-v8av x86 x86_64
# - The build step to execute:
#
# + sudo-deps: as root, install needed Debian packages into builder VM
# + prebuild: patch sources and do other stuff before the build
# + build: perform actual build of APK file
#
# Parse command-line arguments
VERNAME="${1}"
VERCODE="${2}"
ANDROID_ABI="${3}"
BUILDSTEP="${4}"
if [ -z "${VERNAME}" ] || [ -z "${VERCODE}" ] || [ -z "${ANDROID_ABI}" ] ||
[ -z "${BUILDSTEP}" ]; then
echo "ERROR: Command-line arguments are all required to be non-empty!" >&2
exit 1
fi
# Set various architecture-specific identifiers
case "${ANDROID_ABI}" in
arm64-v8a)
FLUTTER_TARGET=android-arm64
NDK_TARGET=aarch64-linux-android
RUST_TARGET=aarch64-linux-android
RUSTDESK_FEATURES='flutter,hwcodec'
;;
armeabi-v7a)
FLUTTER_TARGET=android-arm
NDK_TARGET=arm-linux-androideabi
RUST_TARGET=armv7-linux-androideabi
RUSTDESK_FEATURES='flutter,hwcodec'
;;
x86_64)
FLUTTER_TARGET=android-x64
NDK_TARGET=x86_64-linux-android
RUST_TARGET=x86_64-linux-android
RUSTDESK_FEATURES='flutter'
;;
x86)
FLUTTER_TARGET=android-x86
NDK_TARGET=i686-linux-android
RUST_TARGET=i686-linux-android
RUSTDESK_FEATURES='flutter'
;;
*)
echo "ERROR: Unknown Android ABI '${ANDROID_ABI}'!" >&2
exit 1
;;
esac
# Check ANDROID_SDK_ROOT and sdkmanager present on PATH
if [ ! -d "${ANDROID_SDK_ROOT}" ] || ! command -v sdkmanager 1>/dev/null; then
echo "ERROR: Can not find Android SDK!" >&2
exit 1
fi
# Export necessary variables
export PATH="${PATH}:${HOME}/flutter/bin:${HOME}/depot_tools"
export VCPKG_ROOT="${HOME}/vcpkg"
# Now act depending on build step
# NOTE: F-Droid maintainers require explicit declaration of dependencies
# as root via `Builds.sudo` F-Droid metadata directive:
# https://gitlab.com/fdroid/fdroiddata/-/merge_requests/15343#note_1988918695
case "${BUILDSTEP}" in
prebuild)
# prebuild: patch sources and do other stuff before the build
#
# Extract required versions for NDK, Rust, Flutter from
# '.github/workflows/flutter-build.yml'
#
CARGO_NDK_VERSION="$(yq -r \
.env.CARGO_NDK_VERSION \
.github/workflows/flutter-build.yml)"
FLUTTER_VERSION="$(yq -r \
.env.ANDROID_FLUTTER_VERSION \
.github/workflows/flutter-build.yml)"
if [ -z "${FLUTTER_VERSION}" ]; then
FLUTTER_VERSION="$(yq -r \
.env.FLUTTER_VERSION \
.github/workflows/flutter-build.yml)"
fi
FLUTTER_RUST_BRIDGE_VERSION="$(yq -r \
.env.FLUTTER_RUST_BRIDGE_VERSION \
.github/workflows/flutter-build.yml)"
NDK_VERSION="$(yq -r \
.env.NDK_VERSION \
.github/workflows/flutter-build.yml)"
RUST_VERSION="$(yq -r \
.env.RUST_VERSION \
.github/workflows/flutter-build.yml)"
VCPKG_COMMIT_ID="$(yq -r \
.env.VCPKG_COMMIT_ID \
.github/workflows/flutter-build.yml)"
if [ -z "${CARGO_NDK_VERSION}" ] || [ -z "${FLUTTER_VERSION}" ] ||
[ -z "${FLUTTER_RUST_BRIDGE_VERSION}" ] ||
[ -z "${NDK_VERSION}" ] || [ -z "${RUST_VERSION}" ] ||
[ -z "${VCPKG_COMMIT_ID}" ]; then
echo "ERROR: Can not identify all required versions!" >&2
exit 1
fi
# Map NDK version to revision
NDK_VERSION="$(wget \
-qO- \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2022-11-28" \
'https://api.github.com/repos/android/ndk/releases' |
jq -r ".[] | select(.tag_name == \"${NDK_VERSION}\") | .body | match(\"ndkVersion \\\"(.*)\\\"\").captures[0].string")"
if [ -z "${NDK_VERSION}" ]; then
echo "ERROR: Can not map Android NDK codename to revision!" >&2
exit 1
fi
export ANDROID_NDK_HOME="${ANDROID_SDK_ROOT}/ndk/${NDK_VERSION}"
export ANDROID_NDK_ROOT="${ANDROID_SDK_ROOT}/ndk/${NDK_VERSION}"
#
# Install the components
#
set -e
# Install Android NDK
if [ ! -d "${ANDROID_NDK_ROOT}" ]; then
sdkmanager --install "ndk;${NDK_VERSION}"
fi
# Install Flutter
if [ ! -f "${HOME}/flutter/bin/flutter" ]; then
pushd "${HOME}"
git clone https://github.com/flutter/flutter
pushd flutter
git reset --hard "${FLUTTER_VERSION}"
flutter config --no-analytics
popd # flutter
popd # ${HOME}
fi
# Install Rust
if [ ! -f "${HOME}/rustup/rustup-init.sh" ]; then
pushd "${HOME}"
git clone --depth 1 https://github.com/rust-lang/rustup
popd # ${HOME}
fi
pushd "${HOME}/rustup"
bash rustup-init.sh -y \
--target "${RUST_TARGET}" \
--default-toolchain "${RUST_VERSION}"
popd
if ! command -v cargo 1>/dev/null 2>&1; then
. "${HOME}/.cargo/env"
fi
# Install cargo-ndk
cargo install \
cargo-ndk \
--version "${CARGO_NDK_VERSION}"
# Install rust bridge generator
cargo install cargo-expand
cargo install flutter_rust_bridge_codegen \
--version "${FLUTTER_RUST_BRIDGE_VERSION}" \
--features "uuid"
# Populate native vcpkg dependencies
if [ ! -d "${VCPKG_ROOT}" ]; then
pushd "${HOME}"
git clone \
https://github.com/Microsoft/vcpkg.git
git clone \
https://github.com/Microsoft/vcpkg-tool.git
pushd vcpkg-tool
mkdir build
pushd build
cmake \
-DCMAKE_BUILD_TYPE=Release \
-G 'Ninja' \
-DVCPKG_DEVELOPMENT_WARNINGS=OFF \
..
cmake --build .
popd # build
popd # vcpkg-tool
pushd vcpkg
git reset --hard "${VCPKG_COMMIT_ID}"
cp -a ../vcpkg-tool/build/vcpkg vcpkg
# disable telemetry
touch "vcpkg.disable-metrics"
popd # vcpkg
popd # ${HOME}
fi
# Install depot-tools for x86
if [ "${ANDROID_ABI}" = "x86" ]; then
if [ ! -d "${HOME}/depot_tools" ]; then
pushd "${HOME}"
git clone \
--depth 1 \
https://chromium.googlesource.com/chromium/tools/depot_tools.git
popd # ${HOME}
fi
fi
# Patch the RustDesk sources
git apply res/fdroid/patches/*.patch
sed \
-i \
-e '/gms/d' \
flutter/android/build.gradle \
flutter/android/app/build.gradle
sed \
-i \
-e '/firebase_analytics/d' \
flutter/pubspec.yaml
sed \
-i \
-e '/ firebase/,/ version/d' \
flutter/pubspec.lock
sed \
-i \
-e '/firebase/Id' \
flutter/lib/main.dart
if [ "${FLUTTER_VERSION}" = "3.13.9" ]; then
# Fix for android 3.13.9
# https://github.com/rustdesk/rustdesk/blob/285e974d1a52c891d5fcc28e963d724e085558bc/.github/workflows/flutter-build.yml#L862
sed \
-i \
-e 's/extended_text: .*/extended_text: 11.1.0/' \
-e 's/uni_links_desktop/#uni_links_desktop/g' \
flutter/pubspec.yaml
set --
while read -r _1; do
set -- "$@" "${_1}"
done 0<<.a
$(find flutter/lib/ -type f -name "*dart*")
.a
sed \
-i \
-e 's/textScaler: TextScaler.linear(\(.*\)),/textScaleFactor: \1,/g' \
"$@"
set --
fi
sed -i "s/FLUTTER_VERSION_PLACEHOLDER/${FLUTTER_VERSION}/" flutter-sdk/.gclient
;;
build)
# build: perform actual build of APK file
set -e
#
# Extract required versions for NDK, Rust, Flutter from
# '.github/workflows/flutter-build.yml'
#
FLUTTER_VERSION="$(yq -r \
.env.ANDROID_FLUTTER_VERSION \
.github/workflows/flutter-build.yml)"
if [ -z "${FLUTTER_VERSION}" ]; then
FLUTTER_VERSION="$(yq -r \
.env.FLUTTER_VERSION \
.github/workflows/flutter-build.yml)"
fi
NDK_VERSION="$(yq -r \
.env.NDK_VERSION \
.github/workflows/flutter-build.yml)"
# Map NDK version to revision
NDK_VERSION="$(wget \
-qO- \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2022-11-28" \
'https://api.github.com/repos/android/ndk/releases' |
jq -r ".[] | select(.tag_name == \"${NDK_VERSION}\") | .body | match(\"ndkVersion \\\"(.*)\\\"\").captures[0].string")"
if [ -z "${NDK_VERSION}" ]; then
echo "ERROR: Can not map Android NDK codename to revision!" >&2
exit 1
fi
export ANDROID_NDK_HOME="${ANDROID_SDK_ROOT}/ndk/${NDK_VERSION}"
export ANDROID_NDK_ROOT="${ANDROID_SDK_ROOT}/ndk/${NDK_VERSION}"
if ! command -v cargo 1>/dev/null 2>&1; then
. "${HOME}/.cargo/env"
fi
# Download Flutter dependencies
pushd flutter
flutter packages pub get
popd # flutter
# Generate FFI bindings
flutter_rust_bridge_codegen \
--rust-input ./src/flutter_ffi.rs \
--dart-output ./flutter/lib/generated_bridge.dart
# Build host android deps
bash flutter/build_android_deps.sh "${ANDROID_ABI}"
# Build rustdesk lib
cargo ndk \
--platform 21 \
--target "${RUST_TARGET}" \
--bindgen \
build \
--release \
--features "${RUSTDESK_FEATURES}"
mkdir -p "flutter/android/app/src/main/jniLibs/${ANDROID_ABI}"
cp "target/${RUST_TARGET}/release/liblibrustdesk.so" \
"flutter/android/app/src/main/jniLibs/${ANDROID_ABI}/librustdesk.so"
cp "${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/${NDK_TARGET}/libc++_shared.so" \
"flutter/android/app/src/main/jniLibs/${ANDROID_ABI}/"
"${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip" \
"flutter/android/app/src/main/jniLibs/${ANDROID_ABI}"/*
# Build flutter-jit-release for x86
if [ "${ANDROID_ABI}" = "x86" ]; then
pushd flutter-sdk
echo "## Sync flutter engine sources"
echo "### We need fakeroot because chromium base image is unpacked with weird uid/gid ownership"
sed -i "s/FLUTTER_VERSION_PLACEHOLDER/${FLUTTER_VERSION}/" .gclient
export FAKEROOTDONTTRYCHOWN=1
fakeroot gclient sync
unset FAKEROOTDONTTRYCHOWN
pushd src
echo "## Patch away Google Play dependencies"
rm \
flutter/shell/platform/android/io/flutter/app/FlutterPlayStoreSplitApplication.java \
flutter/shell/platform/android/io/flutter/embedding/engine/deferredcomponents/PlayStoreDeferredComponentManager.java flutter/shell/platform/android/io/flutter/embedding/android/FlutterPlayStoreSplitApplication.java
sed \
-i \
-e '/PlayStore/d' \
flutter/tools/android_lint/project.xml \
flutter/shell/platform/android/BUILD.gn
sed \
-i \
-e '/com.google.android.play/d' \
flutter/tools/androidx/files.json
echo "## Configure android engine build"
flutter/tools/gn \
--android --android-cpu x86 --runtime-mode=jit_release \
--no-goma --no-enable-unittests
echo "## Perform android engine build"
ninja -C out/android_jit_release_x86
echo "## Configure host engine build"
flutter/tools/gn \
--android-cpu x86 --runtime-mode=jit_release \
--no-goma --no-enable-unittests
echo "## Perform android engine build"
ninja -C out/host_jit_release_x86
echo "## Rename host engine"
mv out/host_jit_release_x86 out/host_jit_release
echo "## Mimic jit_release engine to debug to use with flutter build apk"
pushd out/android_jit_release_x86
sed \
-e 's/jit_release/debug/' \
flutter_embedding_jit_release.maven-metadata.xml \
1>flutter_embedding_debug.maven-metadata.xml
sed \
-e 's/jit_release/debug/' \
flutter_embedding_jit_release.pom \
1>flutter_embedding_debug.pom
sed \
-e 's/jit_release/debug/' \
x86_jit_release.maven-metadata.xml \
1>x86_debug.maven-metadata.xml
sed \
-e 's/jit_release/debug/' \
x86_jit_release.pom \
1>x86_debug.pom
cp -a \
flutter_embedding_jit_release-sources.jar \
flutter_embedding_debug-sources.jar
cp -a \
flutter_embedding_jit_release.jar \
flutter_embedding_debug.jar
cp -a \
x86_jit_release.jar \
x86_debug.jar
popd # out/android_jit_release_x86
popd # src
popd # flutter-sdk
echo "# Clean up intermediate engine files and show free space"
rm -rf \
flutter-sdk/src/out/android_jit_release_x86/obj \
flutter-sdk/src/out/host_jit_release/obj
mv flutter-sdk/src/out flutter-out
rm -rf flutter-sdk
mkdir -p flutter-sdk/src/
mv flutter-out flutter-sdk/src/out
fi
# Build the apk
pushd flutter
if [ "${ANDROID_ABI}" = "x86" ]; then
flutter build apk \
--local-engine-src-path="$(readlink -mf "../flutter-sdk/src")" \
--local-engine=android_jit_release_x86 \
--debug \
--build-number="${VERCODE}" \
--build-name="${VERNAME}" \
--target-platform "${FLUTTER_TARGET}"
else
flutter build apk \
--release \
--build-number="${VERCODE}" \
--build-name="${VERNAME}" \
--target-platform "${FLUTTER_TARGET}"
fi
popd # flutter
rm -rf flutter-sdk
# Special step for fdroiddata CI builds to remove .gitconfig
rm -f /home/vagrant/.gitconfig
;;
*)
echo "ERROR: Unknown build step '${BUILDSTEP}'!" >&2
exit 1
;;
esac
# Report success
echo "All done!"

View File

@@ -139,4 +139,4 @@ SPEC CHECKSUMS:
PODFILE CHECKSUM: d4cb12ad5d3bdb3352770b1d3db237584e155156 PODFILE CHECKSUM: d4cb12ad5d3bdb3352770b1d3db237584e155156
COCOAPODS: 1.12.1 COCOAPODS: 1.15.2

View File

@@ -159,7 +159,7 @@
97C146E61CF9000F007C117D /* Project object */ = { 97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject; isa = PBXProject;
attributes = { attributes = {
LastUpgradeCheck = 1430; LastUpgradeCheck = 1510;
ORGANIZATIONNAME = ""; ORGANIZATIONNAME = "";
TargetAttributes = { TargetAttributes = {
97C146ED1CF9000F007C117D = { 97C146ED1CF9000F007C117D = {

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<Scheme <Scheme
LastUpgradeVersion = "1430" LastUpgradeVersion = "1510"
version = "1.3"> version = "1.3">
<BuildAction <BuildAction
parallelizeBuildables = "YES" parallelizeBuildables = "YES"

View File

@@ -2,6 +2,8 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>aps-environment</key>
<string>development</string>
<key>com.apple.developer.networking.wifi-info</key> <key>com.apple.developer.networking.wifi-info</key>
<true/> <true/>
</dict> </dict>

View File

@@ -1,2 +1,2 @@
#!/usr/bin/env bash #!/usr/bin/env bash
cargo build --features flutter --release --target aarch64-apple-ios --lib cargo build --features flutter,hwcodec --release --target aarch64-apple-ios --lib

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,6 @@
import 'dart:convert'; import 'dart:convert';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/models/peer_model.dart'; import 'package:flutter_hbb/models/peer_model.dart';
@@ -188,3 +189,79 @@ class RequestException implements Exception {
return "RequestException, statusCode: $statusCode, error: $cause"; return "RequestException, statusCode: $statusCode, error: $cause";
} }
} }
enum ShareRule {
read(1),
readWrite(2),
fullControl(3);
const ShareRule(this.value);
final int value;
static String desc(int v) {
if (v == ShareRule.read.value) {
return translate('Read-only');
}
if (v == ShareRule.readWrite.value) {
return translate('Read/Write');
}
if (v == ShareRule.fullControl.value) {
return translate('Full Control');
}
return v.toString();
}
static String shortDesc(int v) {
if (v == ShareRule.read.value) {
return 'R';
}
if (v == ShareRule.readWrite.value) {
return 'RW';
}
if (v == ShareRule.fullControl.value) {
return 'F';
}
return v.toString();
}
static ShareRule? fromValue(int v) {
if (v == ShareRule.read.value) {
return ShareRule.read;
}
if (v == ShareRule.readWrite.value) {
return ShareRule.readWrite;
}
if (v == ShareRule.fullControl.value) {
return ShareRule.fullControl;
}
return null;
}
}
class AbProfile {
String guid;
String name;
String owner;
String? note;
int rule;
AbProfile(this.guid, this.name, this.owner, this.note, this.rule);
AbProfile.fromJson(Map<String, dynamic> json)
: guid = json['guid'] ?? '',
name = json['name'] ?? '',
owner = json['owner'] ?? '',
note = json['note'] ?? '',
rule = json['rule'] ?? 0;
}
class AbTag {
String name;
int color;
AbTag(this.name, this.color);
AbTag.fromJson(Map<String, dynamic> json)
: name = json['name'] ?? '',
color = json['color'] ?? '';
}

View File

@@ -10,16 +10,16 @@ class PrivacyModeState {
static void init(String id) { static void init(String id) {
final key = tag(id); final key = tag(id);
if (!Get.isRegistered(tag: key)) { if (!Get.isRegistered<RxString>(tag: key)) {
final RxString state = ''.obs; final RxString state = ''.obs;
Get.put(state, tag: key); Get.put<RxString>(state, tag: key);
} }
} }
static void delete(String id) { static void delete(String id) {
final key = tag(id); final key = tag(id);
if (Get.isRegistered(tag: key)) { if (Get.isRegistered<RxString>(tag: key)) {
Get.delete(tag: key); Get.delete<RxString>(tag: key);
} else { } else {
Get.find<RxString>(tag: key).value = ''; Get.find<RxString>(tag: key).value = '';
} }
@@ -33,9 +33,9 @@ class BlockInputState {
static void init(String id) { static void init(String id) {
final key = tag(id); final key = tag(id);
if (!Get.isRegistered(tag: key)) { if (!Get.isRegistered<RxBool>(tag: key)) {
final RxBool state = false.obs; final RxBool state = false.obs;
Get.put(state, tag: key); Get.put<RxBool>(state, tag: key);
} else { } else {
Get.find<RxBool>(tag: key).value = false; Get.find<RxBool>(tag: key).value = false;
} }
@@ -43,8 +43,8 @@ class BlockInputState {
static void delete(String id) { static void delete(String id) {
final key = tag(id); final key = tag(id);
if (Get.isRegistered(tag: key)) { if (Get.isRegistered<RxBool>(tag: key)) {
Get.delete(tag: key); Get.delete<RxBool>(tag: key);
} }
} }
@@ -56,9 +56,9 @@ class CurrentDisplayState {
static void init(String id) { static void init(String id) {
final key = tag(id); final key = tag(id);
if (!Get.isRegistered(tag: key)) { if (!Get.isRegistered<RxInt>(tag: key)) {
final RxInt state = RxInt(0); final RxInt state = RxInt(0);
Get.put(state, tag: key); Get.put<RxInt>(state, tag: key);
} else { } else {
Get.find<RxInt>(tag: key).value = 0; Get.find<RxInt>(tag: key).value = 0;
} }
@@ -66,8 +66,8 @@ class CurrentDisplayState {
static void delete(String id) { static void delete(String id) {
final key = tag(id); final key = tag(id);
if (Get.isRegistered(tag: key)) { if (Get.isRegistered<RxInt>(tag: key)) {
Get.delete(tag: key); Get.delete<RxInt>(tag: key);
} }
} }
@@ -105,16 +105,16 @@ class ConnectionTypeState {
static void init(String id) { static void init(String id) {
final key = tag(id); final key = tag(id);
if (!Get.isRegistered(tag: key)) { if (!Get.isRegistered<ConnectionType>(tag: key)) {
final ConnectionType collectionType = ConnectionType(); final ConnectionType collectionType = ConnectionType();
Get.put(collectionType, tag: key); Get.put<ConnectionType>(collectionType, tag: key);
} }
} }
static void delete(String id) { static void delete(String id) {
final key = tag(id); final key = tag(id);
if (Get.isRegistered(tag: key)) { if (Get.isRegistered<ConnectionType>(tag: key)) {
Get.delete(tag: key); Get.delete<ConnectionType>(tag: key);
} }
} }
@@ -127,9 +127,9 @@ class FingerprintState {
static void init(String id) { static void init(String id) {
final key = tag(id); final key = tag(id);
if (!Get.isRegistered(tag: key)) { if (!Get.isRegistered<RxString>(tag: key)) {
final RxString state = ''.obs; final RxString state = ''.obs;
Get.put(state, tag: key); Get.put<RxString>(state, tag: key);
} else { } else {
Get.find<RxString>(tag: key).value = ''; Get.find<RxString>(tag: key).value = '';
} }
@@ -137,8 +137,8 @@ class FingerprintState {
static void delete(String id) { static void delete(String id) {
final key = tag(id); final key = tag(id);
if (Get.isRegistered(tag: key)) { if (Get.isRegistered<RxString>(tag: key)) {
Get.delete(tag: key); Get.delete<RxString>(tag: key);
} }
} }
@@ -150,9 +150,9 @@ class ShowRemoteCursorState {
static void init(String id) { static void init(String id) {
final key = tag(id); final key = tag(id);
if (!Get.isRegistered(tag: key)) { if (!Get.isRegistered<RxBool>(tag: key)) {
final RxBool state = false.obs; final RxBool state = false.obs;
Get.put(state, tag: key); Get.put<RxBool>(state, tag: key);
} else { } else {
Get.find<RxBool>(tag: key).value = false; Get.find<RxBool>(tag: key).value = false;
} }
@@ -160,8 +160,31 @@ class ShowRemoteCursorState {
static void delete(String id) { static void delete(String id) {
final key = tag(id); final key = tag(id);
if (Get.isRegistered(tag: key)) { if (Get.isRegistered<RxBool>(tag: key)) {
Get.delete(tag: key); Get.delete<RxBool>(tag: key);
}
}
static RxBool find(String id) => Get.find<RxBool>(tag: tag(id));
}
class ShowRemoteCursorLockState {
static String tag(String id) => 'show_remote_cursor_lock_$id';
static void init(String id) {
final key = tag(id);
if (!Get.isRegistered<RxBool>(tag: key)) {
final RxBool state = false.obs;
Get.put<RxBool>(state, tag: key);
} else {
Get.find<RxBool>(tag: key).value = false;
}
}
static void delete(String id) {
final key = tag(id);
if (Get.isRegistered<RxBool>(tag: key)) {
Get.delete<RxBool>(tag: key);
} }
} }
@@ -173,10 +196,10 @@ class KeyboardEnabledState {
static void init(String id) { static void init(String id) {
final key = tag(id); final key = tag(id);
if (!Get.isRegistered(tag: key)) { if (!Get.isRegistered<RxBool>(tag: key)) {
// Server side, default true // Server side, default true
final RxBool state = true.obs; final RxBool state = true.obs;
Get.put(state, tag: key); Get.put<RxBool>(state, tag: key);
} else { } else {
Get.find<RxBool>(tag: key).value = true; Get.find<RxBool>(tag: key).value = true;
} }
@@ -184,8 +207,8 @@ class KeyboardEnabledState {
static void delete(String id) { static void delete(String id) {
final key = tag(id); final key = tag(id);
if (Get.isRegistered(tag: key)) { if (Get.isRegistered<RxBool>(tag: key)) {
Get.delete(tag: key); Get.delete<RxBool>(tag: key);
} }
} }
@@ -197,9 +220,9 @@ class RemoteCursorMovedState {
static void init(String id) { static void init(String id) {
final key = tag(id); final key = tag(id);
if (!Get.isRegistered(tag: key)) { if (!Get.isRegistered<RxBool>(tag: key)) {
final RxBool state = false.obs; final RxBool state = false.obs;
Get.put(state, tag: key); Get.put<RxBool>(state, tag: key);
} else { } else {
Get.find<RxBool>(tag: key).value = false; Get.find<RxBool>(tag: key).value = false;
} }
@@ -207,8 +230,8 @@ class RemoteCursorMovedState {
static void delete(String id) { static void delete(String id) {
final key = tag(id); final key = tag(id);
if (Get.isRegistered(tag: key)) { if (Get.isRegistered<RxBool>(tag: key)) {
Get.delete(tag: key); Get.delete<RxBool>(tag: key);
} }
} }
@@ -220,9 +243,9 @@ class RemoteCountState {
static void init() { static void init() {
final key = tag(); final key = tag();
if (!Get.isRegistered(tag: key)) { if (!Get.isRegistered<RxInt>(tag: key)) {
final RxInt state = 1.obs; final RxInt state = 1.obs;
Get.put(state, tag: key); Get.put<RxInt>(state, tag: key);
} else { } else {
Get.find<RxInt>(tag: key).value = 1; Get.find<RxInt>(tag: key).value = 1;
} }
@@ -230,8 +253,8 @@ class RemoteCountState {
static void delete() { static void delete() {
final key = tag(); final key = tag();
if (Get.isRegistered(tag: key)) { if (Get.isRegistered<RxInt>(tag: key)) {
Get.delete(tag: key); Get.delete<RxInt>(tag: key);
} }
} }
@@ -243,9 +266,9 @@ class PeerBoolOption {
static void init(String id, String opt, bool Function() init_getter) { static void init(String id, String opt, bool Function() init_getter) {
final key = tag(id, opt); final key = tag(id, opt);
if (!Get.isRegistered(tag: key)) { if (!Get.isRegistered<RxBool>(tag: key)) {
final RxBool value = RxBool(init_getter()); final RxBool value = RxBool(init_getter());
Get.put(value, tag: key); Get.put<RxBool>(value, tag: key);
} else { } else {
Get.find<RxBool>(tag: key).value = init_getter(); Get.find<RxBool>(tag: key).value = init_getter();
} }
@@ -253,8 +276,8 @@ class PeerBoolOption {
static void delete(String id, String opt) { static void delete(String id, String opt) {
final key = tag(id, opt); final key = tag(id, opt);
if (Get.isRegistered(tag: key)) { if (Get.isRegistered<RxBool>(tag: key)) {
Get.delete(tag: key); Get.delete<RxBool>(tag: key);
} }
} }
@@ -267,9 +290,9 @@ class PeerStringOption {
static void init(String id, String opt, String Function() init_getter) { static void init(String id, String opt, String Function() init_getter) {
final key = tag(id, opt); final key = tag(id, opt);
if (!Get.isRegistered(tag: key)) { if (!Get.isRegistered<RxString>(tag: key)) {
final RxString value = RxString(init_getter()); final RxString value = RxString(init_getter());
Get.put(value, tag: key); Get.put<RxString>(value, tag: key);
} else { } else {
Get.find<RxString>(tag: key).value = init_getter(); Get.find<RxString>(tag: key).value = init_getter();
} }
@@ -277,8 +300,8 @@ class PeerStringOption {
static void delete(String id, String opt) { static void delete(String id, String opt) {
final key = tag(id, opt); final key = tag(id, opt);
if (Get.isRegistered(tag: key)) { if (Get.isRegistered<RxString>(tag: key)) {
Get.delete(tag: key); Get.delete<RxString>(tag: key);
} }
} }
@@ -291,9 +314,9 @@ class UnreadChatCountState {
static void init(String id) { static void init(String id) {
final key = tag(id); final key = tag(id);
if (!Get.isRegistered(tag: key)) { if (!Get.isRegistered<RxInt>(tag: key)) {
final RxInt state = RxInt(0); final RxInt state = RxInt(0);
Get.put(state, tag: key); Get.put<RxInt>(state, tag: key);
} else { } else {
Get.find<RxInt>(tag: key).value = 0; Get.find<RxInt>(tag: key).value = 0;
} }
@@ -301,8 +324,8 @@ class UnreadChatCountState {
static void delete(String id) { static void delete(String id) {
final key = tag(id); final key = tag(id);
if (Get.isRegistered(tag: key)) { if (Get.isRegistered<RxInt>(tag: key)) {
Get.delete(tag: key); Get.delete<RxInt>(tag: key);
} }
} }
@@ -315,9 +338,10 @@ initSharedStates(String id) {
CurrentDisplayState.init(id); CurrentDisplayState.init(id);
KeyboardEnabledState.init(id); KeyboardEnabledState.init(id);
ShowRemoteCursorState.init(id); ShowRemoteCursorState.init(id);
ShowRemoteCursorLockState.init(id);
RemoteCursorMovedState.init(id); RemoteCursorMovedState.init(id);
FingerprintState.init(id); FingerprintState.init(id);
PeerBoolOption.init(id, 'zoom-cursor', () => false); PeerBoolOption.init(id, kOptionZoomCursor, () => false);
UnreadChatCountState.init(id); UnreadChatCountState.init(id);
if (isMobile) ConnectionTypeState.init(id); // desktop in other places if (isMobile) ConnectionTypeState.init(id); // desktop in other places
} }
@@ -327,10 +351,11 @@ removeSharedStates(String id) {
BlockInputState.delete(id); BlockInputState.delete(id);
CurrentDisplayState.delete(id); CurrentDisplayState.delete(id);
ShowRemoteCursorState.delete(id); ShowRemoteCursorState.delete(id);
ShowRemoteCursorLockState.delete(id);
KeyboardEnabledState.delete(id); KeyboardEnabledState.delete(id);
RemoteCursorMovedState.delete(id); RemoteCursorMovedState.delete(id);
FingerprintState.delete(id); FingerprintState.delete(id);
PeerBoolOption.delete(id, 'zoom-cursor'); PeerBoolOption.delete(id, kOptionZoomCursor);
UnreadChatCountState.delete(id); UnreadChatCountState.delete(id);
if (isMobile) ConnectionTypeState.delete(id); if (isMobile) ConnectionTypeState.delete(id);
} }

View File

@@ -1,13 +1,18 @@
import 'dart:math'; import 'dart:math';
import 'package:dropdown_button2/dropdown_button2.dart';
import 'package:dynamic_layouts/dynamic_layouts.dart'; import 'package:dynamic_layouts/dynamic_layouts.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hbb/common/formatter/id_formatter.dart'; import 'package:flutter_hbb/common/formatter/id_formatter.dart';
import 'package:flutter_hbb/common/hbbs/hbbs.dart';
import 'package:flutter_hbb/common/widgets/peer_card.dart'; import 'package:flutter_hbb/common/widgets/peer_card.dart';
import 'package:flutter_hbb/common/widgets/peers_view.dart'; import 'package:flutter_hbb/common/widgets/peers_view.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/desktop/widgets/popup_menu.dart'; import 'package:flutter_hbb/desktop/widgets/popup_menu.dart';
import 'package:flutter_hbb/models/ab_model.dart'; import 'package:flutter_hbb/models/ab_model.dart';
import 'package:flutter_hbb/models/platform_model.dart'; import 'package:flutter_hbb/models/platform_model.dart';
import 'package:flutter_hbb/models/state_model.dart';
import 'package:url_launcher/url_launcher_string.dart';
import '../../desktop/widgets/material_mod_popup_menu.dart' as mod_menu; import '../../desktop/widgets/material_mod_popup_menu.dart' as mod_menu;
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:flex_color_picker/flex_color_picker.dart'; import 'package:flex_color_picker/flex_color_picker.dart';
@@ -31,47 +36,42 @@ class AddressBook extends StatefulWidget {
class _AddressBookState extends State<AddressBook> { class _AddressBookState extends State<AddressBook> {
var menuPos = RelativeRect.fill; var menuPos = RelativeRect.fill;
@override
void initState() {
super.initState();
}
@override @override
Widget build(BuildContext context) => Obx(() { Widget build(BuildContext context) => Obx(() {
if (!gFFI.userModel.isLogin) { if (!gFFI.userModel.isLogin) {
return Center( return Center(
child: ElevatedButton( child: ElevatedButton(
onPressed: loginDialog, child: Text(translate("Login")))); onPressed: loginDialog, child: Text(translate("Login"))));
} else if (gFFI.userModel.networkError.isNotEmpty) {
return netWorkErrorWidget();
} else { } else {
if (gFFI.abModel.abLoading.value && gFFI.abModel.emtpy) {
return const Center(
child: CircularProgressIndicator(),
);
}
return Column( return Column(
children: [ children: [
// NOT use Offstage to wrap LinearProgressIndicator // NOT use Offstage to wrap LinearProgressIndicator
if (gFFI.abModel.retrying.value) LinearProgressIndicator(), if (gFFI.abModel.currentAbLoading.value &&
gFFI.abModel.currentAbEmpty)
const LinearProgressIndicator(),
buildErrorBanner(context, buildErrorBanner(context,
loading: gFFI.abModel.abLoading, loading: gFFI.abModel.currentAbLoading,
err: gFFI.abModel.pullError, err: gFFI.abModel.currentAbPullError,
retry: null, retry: null,
close: () => gFFI.abModel.pullError.value = ''), close: () => gFFI.abModel.currentAbPullError.value = ''),
buildErrorBanner(context, buildErrorBanner(context,
loading: gFFI.abModel.abLoading, loading: gFFI.abModel.currentAbLoading,
err: gFFI.abModel.pushError, err: gFFI.abModel.currentAbPushError,
retry: () => gFFI.abModel.pushAb(isRetry: true), retry: null, // remove retry
close: () => gFFI.abModel.pushError.value = ''), close: () => gFFI.abModel.currentAbPushError.value = ''),
Expanded( Expanded(
child: isDesktop child: Obx(() => stateGlobal.isPortrait.isTrue
? _buildAddressBookDesktop() ? _buildAddressBookPortrait()
: _buildAddressBookMobile()) : _buildAddressBookLandscape()),
),
], ],
); );
} }
}); });
Widget _buildAddressBookDesktop() { Widget _buildAddressBookLandscape() {
return Row( return Row(
children: [ children: [
Offstage( Offstage(
@@ -82,19 +82,23 @@ class _AddressBookState extends State<AddressBook> {
border: Border.all( border: Border.all(
color: Theme.of(context).colorScheme.background)), color: Theme.of(context).colorScheme.background)),
child: Container( child: Container(
width: 150, width: 200,
height: double.infinity, height: double.infinity,
padding: const EdgeInsets.all(8.0),
child: Column( child: Column(
children: [ children: [
_buildTagHeader().marginOnly(left: 8.0, right: 0), _buildAbDropdown(),
_buildTagHeader().marginOnly(
left: 8.0,
right: gFFI.abModel.legacyMode.value ? 8.0 : 0,
top: gFFI.abModel.legacyMode.value ? 8.0 : 0),
Expanded( Expanded(
child: Container( child: Container(
width: double.infinity, width: double.infinity,
height: double.infinity, height: double.infinity,
child: _buildTags(), child: _buildTags(),
), ),
) ),
_buildAbPermission(),
], ],
), ),
), ),
@@ -104,7 +108,8 @@ class _AddressBookState extends State<AddressBook> {
); );
} }
Widget _buildAddressBookMobile() { Widget _buildAddressBookPortrait() {
const padding = 8.0;
return Column( return Column(
children: [ children: [
Offstage( Offstage(
@@ -115,10 +120,12 @@ class _AddressBookState extends State<AddressBook> {
border: Border.all( border: Border.all(
color: Theme.of(context).colorScheme.background)), color: Theme.of(context).colorScheme.background)),
child: Container( child: Container(
padding: const EdgeInsets.all(8.0), padding:
const EdgeInsets.fromLTRB(padding, 0, padding, padding),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
_buildAbDropdown(),
_buildTagHeader().marginOnly(left: 8.0, right: 0), _buildTagHeader().marginOnly(left: 8.0, right: 0),
Container( Container(
width: double.infinity, width: double.infinity,
@@ -133,6 +140,163 @@ class _AddressBookState extends State<AddressBook> {
); );
} }
Widget _buildAbPermission() {
icon(IconData data, String tooltip) {
return Tooltip(
message: translate(tooltip),
waitDuration: Duration.zero,
child: Icon(data, size: 12.0).marginSymmetric(horizontal: 2.0));
}
return Obx(() {
if (gFFI.abModel.legacyMode.value) return Offstage();
if (gFFI.abModel.current.isPersonal()) {
return Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
icon(Icons.cloud_off, "Personal"),
],
);
} else {
List<Widget> children = [];
final rule = gFFI.abModel.current.sharedProfile()?.rule;
if (rule == ShareRule.read.value) {
children.add(
icon(Icons.visibility, ShareRule.desc(ShareRule.read.value)));
} else if (rule == ShareRule.readWrite.value) {
children
.add(icon(Icons.edit, ShareRule.desc(ShareRule.readWrite.value)));
} else if (rule == ShareRule.fullControl.value) {
children.add(icon(
Icons.security, ShareRule.desc(ShareRule.fullControl.value)));
}
final owner = gFFI.abModel.current.sharedProfile()?.owner;
if (owner != null) {
children.add(icon(Icons.person, "${translate("Owner")}: $owner"));
}
return Row(
mainAxisAlignment: MainAxisAlignment.end,
children: children,
);
}
});
}
Widget _buildAbDropdown() {
if (gFFI.abModel.legacyMode.value) {
return Offstage();
}
final names = gFFI.abModel.addressBookNames();
if (!names.contains(gFFI.abModel.currentName.value)) {
return Offstage();
}
// order: personal, divider, character order
// https://pub.dev/packages/dropdown_button2#3-dropdownbutton2-with-items-of-different-heights-like-dividers
final personalAddressBookName = gFFI.abModel.personalAddressBookName();
bool contains = names.remove(personalAddressBookName);
names.sort((a, b) => a.toLowerCase().compareTo(b.toLowerCase()));
if (contains) {
names.insert(0, personalAddressBookName);
}
Row buildItem(String e, {bool button = false}) {
return Row(
children: [
Expanded(
child: Tooltip(
waitDuration: Duration(milliseconds: 500),
message: gFFI.abModel.translatedName(e),
child: Text(
gFFI.abModel.translatedName(e),
style: button ? null : TextStyle(fontSize: 14.0),
maxLines: 1,
overflow: TextOverflow.ellipsis,
textAlign: button ? TextAlign.center : null,
)),
),
],
);
}
final items = names
.map((e) => DropdownMenuItem(value: e, child: buildItem(e)))
.toList();
var menuItemStyleData = MenuItemStyleData(height: 36);
if (contains && items.length > 1) {
items.insert(1, DropdownMenuItem(enabled: false, child: Divider()));
List<double> customHeights = List.filled(items.length, 36);
customHeights[1] = 4;
menuItemStyleData = MenuItemStyleData(customHeights: customHeights);
}
final TextEditingController textEditingController = TextEditingController();
final isOptFixed = isOptionFixed(kOptionCurrentAbName);
return DropdownButton2<String>(
value: gFFI.abModel.currentName.value,
onChanged: isOptFixed
? null
: (value) {
if (value != null) {
gFFI.abModel.setCurrentName(value);
bind.setLocalFlutterOption(k: kOptionCurrentAbName, v: value);
}
},
customButton: Obx(() => Container(
height: stateGlobal.isPortrait.isFalse ? 48 : 40,
child: Row(children: [
Expanded(
child:
buildItem(gFFI.abModel.currentName.value, button: true)),
Icon(Icons.arrow_drop_down),
]),
)),
underline: Container(
height: 0.7,
color: Theme.of(context).dividerColor.withOpacity(0.1),
),
menuItemStyleData: menuItemStyleData,
items: items,
isExpanded: true,
isDense: true,
dropdownSearchData: DropdownSearchData(
searchController: textEditingController,
searchInnerWidgetHeight: 50,
searchInnerWidget: Container(
height: 50,
padding: const EdgeInsets.only(
top: 8,
bottom: 4,
right: 8,
left: 8,
),
child: TextFormField(
expands: true,
maxLines: null,
controller: textEditingController,
decoration: InputDecoration(
isDense: true,
contentPadding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 8,
),
hintText: translate('Search'),
hintStyle: const TextStyle(fontSize: 12),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
),
),
),
searchMatchFn: (item, searchValue) {
return item.value
.toString()
.toLowerCase()
.contains(searchValue.toLowerCase());
},
),
);
}
Widget _buildTagHeader() { Widget _buildTagHeader() {
return Row( return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
@@ -154,11 +318,12 @@ class _AddressBookState extends State<AddressBook> {
return Obx(() { return Obx(() {
final List tags; final List tags;
if (gFFI.abModel.sortTags.value) { if (gFFI.abModel.sortTags.value) {
tags = gFFI.abModel.tags.toList(); tags = gFFI.abModel.currentAbTags.toList();
tags.sort(); tags.sort();
} else { } else {
tags = gFFI.abModel.tags; tags = gFFI.abModel.currentAbTags;
} }
final editPermission = gFFI.abModel.current.canWrite();
tagBuilder(String e) { tagBuilder(String e) {
return AddressBookTag( return AddressBookTag(
name: e, name: e,
@@ -169,11 +334,12 @@ class _AddressBookState extends State<AddressBook> {
} else { } else {
gFFI.abModel.selectedTags.add(e); gFFI.abModel.selectedTags.add(e);
} }
}); },
showActionMenu: editPermission);
} }
final gridView = DynamicGridView.builder( gridView(bool isPortrait) => DynamicGridView.builder(
shrinkWrap: isMobile, shrinkWrap: isPortrait,
gridDelegate: SliverGridDelegateWithWrapping(), gridDelegate: SliverGridDelegateWithWrapping(),
itemCount: tags.length, itemCount: tags.length,
itemBuilder: (BuildContext context, int index) { itemBuilder: (BuildContext context, int index) {
@@ -181,9 +347,9 @@ class _AddressBookState extends State<AddressBook> {
return tagBuilder(e); return tagBuilder(e);
}); });
final maxHeight = max(MediaQuery.of(context).size.height / 6, 100.0); final maxHeight = max(MediaQuery.of(context).size.height / 6, 100.0);
return isDesktop return Obx(() => stateGlobal.isPortrait.isFalse
? gridView ? gridView(false)
: LimitedBox(maxHeight: maxHeight, child: gridView); : LimitedBox(maxHeight: maxHeight, child: gridView(true)));
}); });
} }
@@ -193,13 +359,13 @@ class _AddressBookState extends State<AddressBook> {
alignment: Alignment.topLeft, alignment: Alignment.topLeft,
child: AddressBookPeersView( child: AddressBookPeersView(
menuPadding: widget.menuPadding, menuPadding: widget.menuPadding,
initPeers: gFFI.abModel.peers,
)), )),
); );
} }
@protected @protected
MenuEntryBase<String> syncMenuItem() { MenuEntryBase<String> syncMenuItem() {
final isOptFixed = isOptionFixed(syncAbOption);
return MenuEntrySwitch<String>( return MenuEntrySwitch<String>(
switchType: SwitchType.scheckbox, switchType: SwitchType.scheckbox,
text: translate('Sync with recent sessions'), text: translate('Sync with recent sessions'),
@@ -207,14 +373,16 @@ class _AddressBookState extends State<AddressBook> {
return shouldSyncAb(); return shouldSyncAb();
}, },
setter: (bool v) async { setter: (bool v) async {
bind.mainSetLocalOption(key: syncAbOption, value: v ? 'Y' : ''); gFFI.abModel.setShouldAsync(v);
}, },
dismissOnClicked: true, dismissOnClicked: true,
enabled: (!isOptFixed).obs,
); );
} }
@protected @protected
MenuEntryBase<String> sortMenuItem() { MenuEntryBase<String> sortMenuItem() {
final isOptFixed = isOptionFixed(sortAbTagsOption);
return MenuEntrySwitch<String>( return MenuEntrySwitch<String>(
switchType: SwitchType.scheckbox, switchType: SwitchType.scheckbox,
text: translate('Sort tags'), text: translate('Sort tags'),
@@ -222,15 +390,18 @@ class _AddressBookState extends State<AddressBook> {
return shouldSortTags(); return shouldSortTags();
}, },
setter: (bool v) async { setter: (bool v) async {
bind.mainSetLocalOption(key: sortAbTagsOption, value: v ? 'Y' : ''); bind.mainSetLocalOption(
key: sortAbTagsOption, value: v ? 'Y' : defaultOptionNo);
gFFI.abModel.sortTags.value = v; gFFI.abModel.sortTags.value = v;
}, },
dismissOnClicked: true, dismissOnClicked: true,
enabled: (!isOptFixed).obs,
); );
} }
@protected @protected
MenuEntryBase<String> filterMenuItem() { MenuEntryBase<String> filterMenuItem() {
final isOptFixed = isOptionFixed(filterAbTagOption);
return MenuEntrySwitch<String>( return MenuEntrySwitch<String>(
switchType: SwitchType.scheckbox, switchType: SwitchType.scheckbox,
text: translate('Filter by intersection'), text: translate('Filter by intersection'),
@@ -238,21 +409,34 @@ class _AddressBookState extends State<AddressBook> {
return filterAbTagByIntersection(); return filterAbTagByIntersection();
}, },
setter: (bool v) async { setter: (bool v) async {
bind.mainSetLocalOption(key: filterAbTagOption, value: v ? 'Y' : ''); bind.mainSetLocalOption(
key: filterAbTagOption, value: v ? 'Y' : defaultOptionNo);
gFFI.abModel.filterByIntersection.value = v; gFFI.abModel.filterByIntersection.value = v;
}, },
dismissOnClicked: true, dismissOnClicked: true,
enabled: (!isOptFixed).obs,
); );
} }
void _showMenu(RelativeRect pos) { void _showMenu(RelativeRect pos) {
final canWrite = gFFI.abModel.current.canWrite();
final items = [ final items = [
getEntry(translate("Add ID"), abAddId), if (canWrite) getEntry(translate("Add ID"), addIdToCurrentAb),
getEntry(translate("Add Tag"), abAddTag), if (canWrite) getEntry(translate("Add Tag"), abAddTag),
getEntry(translate("Unselect all tags"), gFFI.abModel.unsetSelectedTags), getEntry(translate("Unselect all tags"), gFFI.abModel.unsetSelectedTags),
sortMenuItem(), if (gFFI.abModel.legacyMode.value)
syncMenuItem(), sortMenuItem(), // It's already sorted after pulling down
if (canWrite) syncMenuItem(),
filterMenuItem(), filterMenuItem(),
if (!gFFI.abModel.legacyMode.value && canWrite)
MenuEntryDivider<String>(),
if (!gFFI.abModel.legacyMode.value && canWrite)
getEntry(translate("ab_web_console_tip"), () async {
final url = await bind.mainGetApiServer();
if (await canLaunchUrlString(url)) {
launchUrlString(url);
}
}),
]; ];
mod_menu.showMenu( mod_menu.showMenu(
@@ -271,17 +455,20 @@ class _AddressBookState extends State<AddressBook> {
); );
} }
void abAddId() async { void addIdToCurrentAb() async {
if (gFFI.abModel.isFull(true)) { if (gFFI.abModel.isCurrentAbFull(true)) {
return; return;
} }
var isInProgress = false; var isInProgress = false;
var passwordVisible = false;
IDTextEditingController idController = IDTextEditingController(text: ''); IDTextEditingController idController = IDTextEditingController(text: '');
TextEditingController aliasController = TextEditingController(text: ''); TextEditingController aliasController = TextEditingController(text: '');
final tags = List.of(gFFI.abModel.tags); TextEditingController passwordController = TextEditingController(text: '');
final tags = List.of(gFFI.abModel.currentAbTags);
var selectedTag = List<dynamic>.empty(growable: true).obs; var selectedTag = List<dynamic>.empty(growable: true).obs;
final style = TextStyle(fontSize: 14.0); final style = TextStyle(fontSize: 14.0);
String? errorMsg; String? errorMsg;
final isCurrentAbShared = !gFFI.abModel.current.isPersonal();
gFFI.dialogManager.show((setState, close, context) { gFFI.dialogManager.show((setState, close, context) {
submit() async { submit() async {
@@ -293,22 +480,51 @@ class _AddressBookState extends State<AddressBook> {
if (id.isEmpty) { if (id.isEmpty) {
// pass // pass
} else { } else {
if (gFFI.abModel.idContainBy(id)) { if (gFFI.abModel.idContainByCurrent(id)) {
setState(() { setState(() {
isInProgress = false; isInProgress = false;
errorMsg = translate('ID already exists'); errorMsg = translate('ID already exists');
}); });
return; return;
} }
gFFI.abModel.addId(id, aliasController.text.trim(), selectedTag); var password = '';
gFFI.abModel.pushAb(); if (isCurrentAbShared) {
this.setState(() {}); password = passwordController.text;
}
String? errMsg2 = await gFFI.abModel.addIdToCurrent(
id, aliasController.text.trim(), password, selectedTag);
if (errMsg2 != null) {
setState(() {
isInProgress = false;
errorMsg = errMsg2;
});
return;
}
// final currentPeers // final currentPeers
} }
close(); close();
} }
double marginBottom = 4; double marginBottom = 4;
row({required Widget lable, required Widget input}) {
makeChild(bool isPortrait) => Row(
children: [
!isPortrait
? ConstrainedBox(
constraints: const BoxConstraints(minWidth: 100),
child: lable.marginOnly(right: 10))
: SizedBox.shrink(),
Expanded(
child: ConstrainedBox(
constraints: const BoxConstraints(minWidth: 200),
child: input),
),
],
).marginOnly(bottom: !isPortrait ? 8 : 0);
return Obx(() => makeChild(stateGlobal.isPortrait.isTrue));
}
return CustomAlertDialog( return CustomAlertDialog(
title: Text(translate("Add ID")), title: Text(translate("Add ID")),
content: Column( content: Column(
@@ -316,66 +532,112 @@ class _AddressBookState extends State<AddressBook> {
children: [ children: [
Column( Column(
children: [ children: [
Align( row(
alignment: Alignment.centerLeft, lable: Row(
child: Row( children: [
children: [ Text(
Text( '*',
'*', style: TextStyle(color: Colors.red, fontSize: 14),
style: TextStyle(color: Colors.red, fontSize: 14), ),
), Text(
Text( 'ID',
'ID', style: style,
style: style, ),
), ],
], ),
), input: Obx(() => TextField(
).marginOnly(bottom: marginBottom), controller: idController,
TextField( inputFormatters: [IDTextInputFormatter()],
controller: idController, decoration: InputDecoration(
inputFormatters: [IDTextInputFormatter()], labelText: stateGlobal.isPortrait.isFalse
decoration: InputDecoration(errorText: errorMsg), ? null
), : translate('ID'),
Align( errorText: errorMsg,
alignment: Alignment.centerLeft, errorMaxLines: 5),
child: Text( ))),
row(
lable: Text(
translate('Alias'), translate('Alias'),
style: style, style: style,
), ),
).marginOnly(top: 8, bottom: marginBottom), input: Obx(() => TextField(
TextField( controller: aliasController,
controller: aliasController, decoration: InputDecoration(
labelText: stateGlobal.isPortrait.isFalse
? null
: translate('Alias'),
),
)),
), ),
Align( if (isCurrentAbShared)
alignment: Alignment.centerLeft, row(
child: Text( lable: Text(
translate('Tags'), translate('Password'),
style: style, style: style,
),
input: Obx(
() => TextField(
controller: passwordController,
obscureText: !passwordVisible,
decoration: InputDecoration(
labelText: stateGlobal.isPortrait.isFalse
? null
: translate('Password'),
suffixIcon: IconButton(
icon: Icon(
passwordVisible
? Icons.visibility
: Icons.visibility_off,
color: MyTheme.lightTheme.primaryColor),
onPressed: () {
setState(() {
passwordVisible = !passwordVisible;
});
},
),
),
),
)),
if (gFFI.abModel.currentAbTags.isNotEmpty)
Align(
alignment: Alignment.centerLeft,
child: Text(
translate('Tags'),
style: style,
),
).marginOnly(top: 8, bottom: marginBottom),
if (gFFI.abModel.currentAbTags.isNotEmpty)
Align(
alignment: Alignment.centerLeft,
child: Wrap(
children: tags
.map((e) => AddressBookTag(
name: e,
tags: selectedTag,
onTap: () {
if (selectedTag.contains(e)) {
selectedTag.remove(e);
} else {
selectedTag.add(e);
}
},
showActionMenu: false))
.toList(growable: false),
),
), ),
).marginOnly(top: 8, bottom: marginBottom),
Align(
alignment: Alignment.centerLeft,
child: Wrap(
children: tags
.map((e) => AddressBookTag(
name: e,
tags: selectedTag,
onTap: () {
if (selectedTag.contains(e)) {
selectedTag.remove(e);
} else {
selectedTag.add(e);
}
},
showActionMenu: false))
.toList(growable: false),
),
),
], ],
), ),
const SizedBox( const SizedBox(
height: 4.0, height: 4.0,
), ),
if (!gFFI.abModel.current.isPersonal())
Row(children: [
Icon(Icons.info, color: Colors.amber).marginOnly(right: 4),
Text(
translate('share_warning_tip'),
style: TextStyle(fontSize: 12),
)
]).marginSymmetric(vertical: 10),
// NOT use Offstage to wrap LinearProgressIndicator // NOT use Offstage to wrap LinearProgressIndicator
if (isInProgress) const LinearProgressIndicator(), if (isInProgress) const LinearProgressIndicator(),
], ],
@@ -407,10 +669,7 @@ class _AddressBookState extends State<AddressBook> {
} else { } else {
final tags = field.trim().split(RegExp(r"[\s,;\n]+")); final tags = field.trim().split(RegExp(r"[\s,;\n]+"));
field = tags.join(','); field = tags.join(',');
for (final tag in tags) { gFFI.abModel.addTags(tags);
gFFI.abModel.addTag(tag);
}
gFFI.abModel.pushAb();
// final currentPeers // final currentPeers
} }
close(); close();
@@ -491,7 +750,7 @@ class AddressBookTag extends StatelessWidget {
child: Obx(() => Container( child: Obx(() => Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: tags.contains(name) color: tags.contains(name)
? gFFI.abModel.getTagColor(name) ? gFFI.abModel.getCurrentAbTagColor(name)
: Theme.of(context).colorScheme.background, : Theme.of(context).colorScheme.background,
borderRadius: BorderRadius.circular(4)), borderRadius: BorderRadius.circular(4)),
margin: const EdgeInsets.symmetric(horizontal: 4.0, vertical: 4.0), margin: const EdgeInsets.symmetric(horizontal: 4.0, vertical: 4.0),
@@ -506,7 +765,7 @@ class AddressBookTag extends StatelessWidget {
shape: BoxShape.circle, shape: BoxShape.circle,
color: tags.contains(name) color: tags.contains(name)
? Colors.white ? Colors.white
: gFFI.abModel.getTagColor(name)), : gFFI.abModel.getCurrentAbTagColor(name)),
).marginOnly(right: radius / 2), ).marginOnly(right: radius / 2),
Expanded( Expanded(
child: Text(name, child: Text(name,
@@ -530,7 +789,8 @@ class AddressBookTag extends StatelessWidget {
if (newName == null || newName.isEmpty) { if (newName == null || newName.isEmpty) {
return translate('Can not be empty'); return translate('Can not be empty');
} }
if (newName != name && gFFI.abModel.tags.contains(newName)) { if (newName != name &&
gFFI.abModel.currentAbTags.contains(newName)) {
return translate('Already exists'); return translate('Already exists');
} }
return null; return null;
@@ -538,7 +798,6 @@ class AddressBookTag extends StatelessWidget {
onSubmit: (String newName) { onSubmit: (String newName) {
if (name != newName) { if (name != newName) {
gFFI.abModel.renameTag(name, newName); gFFI.abModel.renameTag(name, newName);
gFFI.abModel.pushAb();
} }
Future.delayed(Duration.zero, () => Get.back()); Future.delayed(Duration.zero, () => Get.back());
}, },
@@ -548,7 +807,7 @@ class AddressBookTag extends StatelessWidget {
}), }),
getEntry(translate(translate('Change Color')), () async { getEntry(translate(translate('Change Color')), () async {
final model = gFFI.abModel; final model = gFFI.abModel;
Color oldColor = model.getTagColor(name); Color oldColor = model.getCurrentAbTagColor(name);
Color newColor = await showColorPickerDialog( Color newColor = await showColorPickerDialog(
context, context,
oldColor, oldColor,
@@ -567,12 +826,10 @@ class AddressBookTag extends StatelessWidget {
); );
if (oldColor != newColor) { if (oldColor != newColor) {
model.setTagColor(name, newColor); model.setTagColor(name, newColor);
model.pushAb();
} }
}), }),
getEntry(translate("Delete"), () { getEntry(translate("Delete"), () {
gFFI.abModel.deleteTag(name); gFFI.abModel.deleteTag(name);
gFFI.abModel.pushAb();
Future.delayed(Duration.zero, () => Get.back()); Future.delayed(Duration.zero, () => Get.back());
}), }),
]; ];

View File

@@ -0,0 +1,81 @@
import 'package:flutter/material.dart';
import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/models/platform_model.dart';
const _kWindowsSystemSound = 'System Sound';
typedef AudioINputSetDevice = void Function(String device);
typedef AudioInputBuilder = Widget Function(
List<String> devices, String currentDevice, AudioINputSetDevice setDevice);
class AudioInput extends StatelessWidget {
final AudioInputBuilder builder;
final bool isCm;
final bool isVoiceCall;
const AudioInput(
{Key? key,
required this.builder,
required this.isCm,
required this.isVoiceCall})
: super(key: key);
static String getDefault() {
if (isWindows) return translate('System Sound');
return '';
}
static Future<String> getAudioInput(bool isCm, bool isVoiceCall) {
if (isVoiceCall) {
return bind.getVoiceCallInputDevice(isCm: isCm);
} else {
return bind.mainGetOption(key: 'audio-input');
}
}
static Future<String> getValue(bool isCm, bool isVoiceCall) async {
String device = await getAudioInput(isCm, isVoiceCall);
if (device.isNotEmpty) {
return device;
} else {
return getDefault();
}
}
static Future<void> setDevice(
String device, bool isCm, bool isVoiceCall) async {
if (device == getDefault()) device = '';
if (isVoiceCall) {
await bind.setVoiceCallInputDevice(isCm: isCm, device: device);
} else {
await bind.mainSetOption(key: 'audio-input', value: device);
}
}
static Future<Map<String, Object>> getDevicesInfo(
bool isCm, bool isVoiceCall) async {
List<String> devices = (await bind.mainGetSoundInputs()).toList();
if (isWindows) {
devices.insert(0, translate(_kWindowsSystemSound));
}
String current = await getValue(isCm, isVoiceCall);
return {'devices': devices, 'current': current};
}
@override
Widget build(BuildContext context) {
return futureBuilder(
future: getDevicesInfo(isCm, isVoiceCall),
hasData: (data) {
String currentDevice = data['current'];
List<String> devices = data['devices'] as List<String>;
if (devices.isEmpty) {
return const Offstage();
}
return builder(devices, currentDevice, (devices) {
setDevice(devices, isCm, isVoiceCall);
});
},
);
}
}

View File

@@ -9,9 +9,6 @@ import 'package:flutter_hbb/common/widgets/peer_card.dart';
Future<List<Peer>> getAllPeers() async { Future<List<Peer>> getAllPeers() async {
Map<String, dynamic> recentPeers = jsonDecode(bind.mainLoadRecentPeersSync()); Map<String, dynamic> recentPeers = jsonDecode(bind.mainLoadRecentPeersSync());
Map<String, dynamic> lanPeers = jsonDecode(bind.mainLoadLanPeersSync()); Map<String, dynamic> lanPeers = jsonDecode(bind.mainLoadLanPeersSync());
Map<String, dynamic> abPeers = jsonDecode(bind.mainLoadAbSync());
Map<String, dynamic> groupPeers = jsonDecode(bind.mainLoadGroupSync());
Map<String, dynamic> combinedPeers = {}; Map<String, dynamic> combinedPeers = {};
void mergePeers(Map<String, dynamic> peers) { void mergePeers(Map<String, dynamic> peers) {
@@ -42,8 +39,16 @@ Future<List<Peer>> getAllPeers() async {
mergePeers(recentPeers); mergePeers(recentPeers);
mergePeers(lanPeers); mergePeers(lanPeers);
mergePeers(abPeers); for (var p in gFFI.abModel.allPeers()) {
mergePeers(groupPeers); if (!combinedPeers.containsKey(p.id)) {
combinedPeers[p.id] = p.toJson();
}
}
for (var p in gFFI.groupModel.peers.map((e) => Peer.copy(e)).toList()) {
if (!combinedPeers.containsKey(p.id)) {
combinedPeers[p.id] = p.toJson();
}
}
List<Peer> parsedPeers = []; List<Peer> parsedPeers = [];
@@ -181,10 +186,10 @@ class AutocompletePeerTileState extends State<AutocompletePeerTile> {
], ],
)))); ))));
final colors = _frontN(widget.peer.tags, 25) final colors = _frontN(widget.peer.tags, 25)
.map((e) => gFFI.abModel.getTagColor(e)) .map((e) => gFFI.abModel.getCurrentAbTagColor(e))
.toList(); .toList();
return Tooltip( return Tooltip(
message: isMobile message: !(isDesktop || isWebDesktop)
? '' ? ''
: widget.peer.tags.isNotEmpty : widget.peer.tags.isNotEmpty
? '${translate('Tags')}: ${widget.peer.tags.join(', ')}' ? '${translate('Tags')}: ${widget.peer.tags.join(', ')}'

View File

@@ -0,0 +1,38 @@
import 'package:auto_size_text/auto_size_text.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../common.dart';
Widget getConnectionPageTitle(BuildContext context, bool isWeb) {
return Row(
children: [
Expanded(
child: Row(
children: [
AutoSizeText(
translate('Control Remote Desktop'),
maxLines: 1,
style: Theme.of(context)
.textTheme
.titleLarge
?.merge(TextStyle(height: 1)),
).marginOnly(right: 4),
Tooltip(
waitDuration: Duration(milliseconds: 300),
message: translate(isWeb ? "web_id_input_tip" : "id_input_tip"),
child: Icon(
Icons.help_outline_outlined,
size: 16,
color: Theme.of(context)
.textTheme
.titleLarge
?.color
?.withOpacity(0.5),
),
),
],
)),
],
);
}

View File

@@ -14,7 +14,11 @@ class UppercaseValidationRule extends ValidationRule {
String get name => translate('uppercase'); String get name => translate('uppercase');
@override @override
bool validate(String value) { bool validate(String value) {
return value.contains(RegExp(r'[A-Z]')); return value.runes.any((int rune) {
var character = String.fromCharCode(rune);
return character.toUpperCase() == character &&
character.toLowerCase() != character;
});
} }
} }
@@ -24,7 +28,11 @@ class LowercaseValidationRule extends ValidationRule {
@override @override
bool validate(String value) { bool validate(String value) {
return value.contains(RegExp(r'[a-z]')); return value.runes.any((int rune) {
var character = String.fromCharCode(rune);
return character.toLowerCase() == character &&
character.toUpperCase() != character;
});
} }
} }

View File

@@ -1,12 +1,16 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:io';
import 'package:bot_toast/bot_toast.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_hbb/common/shared_state.dart'; import 'package:flutter_hbb/common/shared_state.dart';
import 'package:flutter_hbb/common/widgets/setting_widgets.dart'; import 'package:flutter_hbb/common/widgets/setting_widgets.dart';
import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/models/peer_model.dart';
import 'package:flutter_hbb/models/peer_tab_model.dart';
import 'package:flutter_hbb/models/state_model.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:qr_flutter/qr_flutter.dart'; import 'package:qr_flutter/qr_flutter.dart';
@@ -78,7 +82,7 @@ void changeIdDialog() {
final Iterable violations = rules.where((r) => !r.validate(newId)); final Iterable violations = rules.where((r) => !r.validate(newId));
if (violations.isNotEmpty) { if (violations.isNotEmpty) {
setState(() { setState(() {
msg = isDesktop msg = (isDesktop || isWebDesktop)
? '${translate('Prompt')}: ${violations.map((r) => r.name).join(', ')}' ? '${translate('Prompt')}: ${violations.map((r) => r.name).join(', ')}'
: violations.map((r) => r.name).join(', '); : violations.map((r) => r.name).join(', ');
}); });
@@ -103,7 +107,7 @@ void changeIdDialog() {
} }
setState(() { setState(() {
isInProgress = false; isInProgress = false;
msg = isDesktop msg = (isDesktop || isWebDesktop)
? '${translate('Prompt')}: ${translate(status)}' ? '${translate('Prompt')}: ${translate(status)}'
: translate(status); : translate(status);
}); });
@@ -140,7 +144,7 @@ void changeIdDialog() {
const SizedBox( const SizedBox(
height: 8.0, height: 8.0,
), ),
isDesktop (isDesktop || isWebDesktop)
? Obx(() => Wrap( ? Obx(() => Wrap(
runSpacing: 8, runSpacing: 8,
spacing: 4, spacing: 4,
@@ -175,11 +179,14 @@ void changeIdDialog() {
} }
void changeWhiteList({Function()? callback}) async { void changeWhiteList({Function()? callback}) async {
var newWhiteList = (await bind.mainGetOption(key: 'whitelist')).split(','); final curWhiteList = await bind.mainGetOption(key: kOptionWhitelist);
var newWhiteListField = newWhiteList.join('\n'); var newWhiteListField = curWhiteList == defaultOptionWhitelist
? ''
: curWhiteList.split(',').join('\n');
var controller = TextEditingController(text: newWhiteListField); var controller = TextEditingController(text: newWhiteListField);
var msg = ""; var msg = "";
var isInProgress = false; var isInProgress = false;
final isOptFixed = isOptionFixed(kOptionWhitelist);
gFFI.dialogManager.show((setState, close, context) { gFFI.dialogManager.show((setState, close, context) {
return CustomAlertDialog( return CustomAlertDialog(
title: Text(translate("IP Whitelisting")), title: Text(translate("IP Whitelisting")),
@@ -199,6 +206,7 @@ void changeWhiteList({Function()? callback}) async {
errorText: msg.isEmpty ? null : translate(msg), errorText: msg.isEmpty ? null : translate(msg),
), ),
controller: controller, controller: controller,
enabled: !isOptFixed,
autofocus: true), autofocus: true),
), ),
], ],
@@ -212,45 +220,53 @@ void changeWhiteList({Function()? callback}) async {
), ),
actions: [ actions: [
dialogButton("Cancel", onPressed: close, isOutline: true), dialogButton("Cancel", onPressed: close, isOutline: true),
dialogButton("Clear", onPressed: () async { if (!isOptFixed)
await bind.mainSetOption(key: 'whitelist', value: ''); dialogButton("Clear", onPressed: () async {
callback?.call(); await bind.mainSetOption(
close(); key: kOptionWhitelist, value: defaultOptionWhitelist);
}, isOutline: true),
dialogButton(
"OK",
onPressed: () async {
setState(() {
msg = "";
isInProgress = true;
});
newWhiteListField = controller.text.trim();
var newWhiteList = "";
if (newWhiteListField.isEmpty) {
// pass
} else {
final ips = newWhiteListField.trim().split(RegExp(r"[\s,;\n]+"));
// test ip
final ipMatch = RegExp(
r"^(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)(\/([1-9]|[1-2][0-9]|3[0-2])){0,1}$");
final ipv6Match = RegExp(
r"^(((?:[0-9A-Fa-f]{1,4}))*((?::[0-9A-Fa-f]{1,4}))*::((?:[0-9A-Fa-f]{1,4}))*((?::[0-9A-Fa-f]{1,4}))*|((?:[0-9A-Fa-f]{1,4}))((?::[0-9A-Fa-f]{1,4})){7})(\/([1-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8])){0,1}$");
for (final ip in ips) {
if (!ipMatch.hasMatch(ip) && !ipv6Match.hasMatch(ip)) {
msg = "${translate("Invalid IP")} $ip";
setState(() {
isInProgress = false;
});
return;
}
}
newWhiteList = ips.join(',');
}
await bind.mainSetOption(key: 'whitelist', value: newWhiteList);
callback?.call(); callback?.call();
close(); close();
}, }, isOutline: true),
), if (!isOptFixed)
dialogButton(
"OK",
onPressed: () async {
setState(() {
msg = "";
isInProgress = true;
});
newWhiteListField = controller.text.trim();
var newWhiteList = "";
if (newWhiteListField.isEmpty) {
// pass
} else {
final ips =
newWhiteListField.trim().split(RegExp(r"[\s,;\n]+"));
// test ip
final ipMatch = RegExp(
r"^(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)(\/([1-9]|[1-2][0-9]|3[0-2])){0,1}$");
final ipv6Match = RegExp(
r"^(((?:[0-9A-Fa-f]{1,4}))*((?::[0-9A-Fa-f]{1,4}))*::((?:[0-9A-Fa-f]{1,4}))*((?::[0-9A-Fa-f]{1,4}))*|((?:[0-9A-Fa-f]{1,4}))((?::[0-9A-Fa-f]{1,4})){7})(\/([1-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8])){0,1}$");
for (final ip in ips) {
if (!ipMatch.hasMatch(ip) && !ipv6Match.hasMatch(ip)) {
msg = "${translate("Invalid IP")} $ip";
setState(() {
isInProgress = false;
});
return;
}
}
newWhiteList = ips.join(',');
}
if (newWhiteList.trim().isEmpty) {
newWhiteList = defaultOptionWhitelist;
}
await bind.mainSetOption(
key: kOptionWhitelist, value: newWhiteList);
callback?.call();
close();
},
),
], ],
onCancel: close, onCancel: close,
); );
@@ -296,7 +312,7 @@ Future<String> changeDirectAccessPort(
dialogButton("Cancel", onPressed: close, isOutline: true), dialogButton("Cancel", onPressed: close, isOutline: true),
dialogButton("OK", onPressed: () async { dialogButton("OK", onPressed: () async {
await bind.mainSetOption( await bind.mainSetOption(
key: 'direct-access-port', value: controller.text); key: kOptionDirectAccessPort, value: controller.text);
close(); close();
}), }),
], ],
@@ -343,7 +359,7 @@ Future<String> changeAutoDisconnectTimeout(String old) async {
dialogButton("Cancel", onPressed: close, isOutline: true), dialogButton("Cancel", onPressed: close, isOutline: true),
dialogButton("OK", onPressed: () async { dialogButton("OK", onPressed: () async {
await bind.mainSetOption( await bind.mainSetOption(
key: 'auto-disconnect-timeout', value: controller.text); key: kOptionAutoDisconnectTimeout, value: controller.text);
close(); close();
}), }),
], ],
@@ -365,6 +381,7 @@ class DialogTextField extends StatelessWidget {
final FocusNode? focusNode; final FocusNode? focusNode;
final TextInputType? keyboardType; final TextInputType? keyboardType;
final List<TextInputFormatter>? inputFormatters; final List<TextInputFormatter>? inputFormatters;
final int? maxLength;
static const kUsernameTitle = 'Username'; static const kUsernameTitle = 'Username';
static const kUsernameIcon = Icon(Icons.account_circle_outlined); static const kUsernameIcon = Icon(Icons.account_circle_outlined);
@@ -382,6 +399,7 @@ class DialogTextField extends StatelessWidget {
this.hintText, this.hintText,
this.keyboardType, this.keyboardType,
this.inputFormatters, this.inputFormatters,
this.maxLength,
required this.title, required this.title,
required this.controller}) required this.controller})
: super(key: key); : super(key: key);
@@ -408,6 +426,7 @@ class DialogTextField extends StatelessWidget {
obscureText: obscureText, obscureText: obscureText,
keyboardType: keyboardType, keyboardType: keyboardType,
inputFormatters: inputFormatters, inputFormatters: inputFormatters,
maxLength: maxLength,
), ),
), ),
], ],
@@ -622,7 +641,7 @@ class _DialogVerificationCodeField extends State<DialogVerificationCodeField> {
// software secure keyboard will take the focus since flutter 3.13 // software secure keyboard will take the focus since flutter 3.13
// request focus again when android account password obtain focus // request focus again when android account password obtain focus
if (Platform.isAndroid && widget.reRequestFocus) { if (isAndroid && widget.reRequestFocus) {
_focusNode.addListener(() { _focusNode.addListener(() {
if (_focusNode.hasFocus) { if (_focusNode.hasFocus) {
_timerReRequestFocus?.cancel(); _timerReRequestFocus?.cancel();
@@ -664,6 +683,8 @@ class PasswordWidget extends StatefulWidget {
this.reRequestFocus = false, this.reRequestFocus = false,
this.hintText, this.hintText,
this.errorText, this.errorText,
this.title,
this.maxLength,
}) : super(key: key); }) : super(key: key);
final TextEditingController controller; final TextEditingController controller;
@@ -671,6 +692,8 @@ class PasswordWidget extends StatefulWidget {
final bool reRequestFocus; final bool reRequestFocus;
final String? hintText; final String? hintText;
final String? errorText; final String? errorText;
final String? title;
final int? maxLength;
@override @override
State<PasswordWidget> createState() => _PasswordWidgetState(); State<PasswordWidget> createState() => _PasswordWidgetState();
@@ -691,7 +714,7 @@ class _PasswordWidgetState extends State<PasswordWidget> {
} }
// software secure keyboard will take the focus since flutter 3.13 // software secure keyboard will take the focus since flutter 3.13
// request focus again when android account password obtain focus // request focus again when android account password obtain focus
if (Platform.isAndroid && widget.reRequestFocus) { if (isAndroid && widget.reRequestFocus) {
_focusNode.addListener(() { _focusNode.addListener(() {
if (_focusNode.hasFocus) { if (_focusNode.hasFocus) {
_timerReRequestFocus?.cancel(); _timerReRequestFocus?.cancel();
@@ -714,7 +737,7 @@ class _PasswordWidgetState extends State<PasswordWidget> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return DialogTextField( return DialogTextField(
title: translate(DialogTextField.kPasswordTitle), title: translate(widget.title ?? DialogTextField.kPasswordTitle),
hintText: translate(widget.hintText ?? 'Enter your password'), hintText: translate(widget.hintText ?? 'Enter your password'),
controller: widget.controller, controller: widget.controller,
prefixIcon: DialogTextField.kPasswordIcon, prefixIcon: DialogTextField.kPasswordIcon,
@@ -733,6 +756,7 @@ class _PasswordWidgetState extends State<PasswordWidget> {
obscureText: !_passwordVisible, obscureText: !_passwordVisible,
errorText: widget.errorText, errorText: widget.errorText,
focusNode: _focusNode, focusNode: _focusNode,
maxLength: widget.maxLength,
); );
} }
} }
@@ -1106,7 +1130,7 @@ void showRequestElevationDialog(
errorText: errPwd.isEmpty ? null : errPwd.value, errorText: errPwd.isEmpty ? null : errPwd.value,
), ),
], ],
).marginOnly(left: isDesktop ? 35 : 0), ).marginOnly(left: stateGlobal.isPortrait.isFalse ? 35 : 0),
).marginOnly(top: 10), ).marginOnly(top: 10),
], ],
), ),
@@ -1583,7 +1607,7 @@ customImageQualityDialog(SessionID sessionId, String id, FFI ffi) async {
msgBoxCommon(ffi.dialogManager, 'Custom Image Quality', content, [btnClose]); msgBoxCommon(ffi.dialogManager, 'Custom Image Quality', content, [btnClose]);
} }
void deletePeerConfirmDialog(Function onSubmit, String title) async { void deleteConfirmDialog(Function onSubmit, String title) async {
gFFI.dialogManager.show( gFFI.dialogManager.show(
(setState, close, context) { (setState, close, context) {
submit() async { submit() async {
@@ -1631,7 +1655,7 @@ void editAbTagDialog(
List<dynamic> currentTags, Function(List<dynamic>) onSubmit) { List<dynamic> currentTags, Function(List<dynamic>) onSubmit) {
var isInProgress = false; var isInProgress = false;
final tags = List.of(gFFI.abModel.tags); final tags = List.of(gFFI.abModel.currentAbTags);
var selectedTag = currentTags.obs; var selectedTag = currentTags.obs;
gFFI.dialogManager.show((setState, close, context) { gFFI.dialogManager.show((setState, close, context) {
@@ -1751,9 +1775,70 @@ void renameDialog(
}); });
} }
void changeBot({Function()? callback}) async {
if (bind.mainHasValidBotSync()) {
await bind.mainSetOption(key: "bot", value: "");
callback?.call();
return;
}
String errorText = '';
bool loading = false;
final controller = TextEditingController();
gFFI.dialogManager.show((setState, close, context) {
onVerify() async {
final token = controller.text.trim();
if (token == "") return;
loading = true;
errorText = '';
setState(() {});
final error = await bind.mainVerifyBot(token: token);
if (error == "") {
callback?.call();
close();
} else {
errorText = translate(error);
loading = false;
setState(() {});
}
}
final codeField = TextField(
autofocus: true,
controller: controller,
decoration: InputDecoration(
hintText: translate('Token'),
),
);
return CustomAlertDialog(
title: Text(translate("Telegram bot")),
content: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SelectableText(translate("enable-bot-desc"),
style: TextStyle(fontSize: 12))
.marginOnly(bottom: 12),
Row(children: [Expanded(child: codeField)]),
if (errorText != '')
Text(errorText, style: TextStyle(color: Colors.red))
.marginOnly(top: 12),
],
),
actions: [
dialogButton("Cancel", onPressed: close, isOutline: true),
loading
? CircularProgressIndicator()
: dialogButton("OK", onPressed: onVerify),
],
onCancel: close,
);
});
}
void change2fa({Function()? callback}) async { void change2fa({Function()? callback}) async {
if (bind.mainHasValid2FaSync()) { if (bind.mainHasValid2FaSync()) {
await bind.mainSetOption(key: "2fa", value: ""); await bind.mainSetOption(key: "2fa", value: "");
await bind.mainClearTrustedDevices();
callback?.call(); callback?.call();
return; return;
} }
@@ -1821,6 +1906,7 @@ void enter2FaDialog(
SessionID sessionId, OverlayDialogManager dialogManager) async { SessionID sessionId, OverlayDialogManager dialogManager) async {
final controller = TextEditingController(); final controller = TextEditingController();
final RxBool submitReady = false.obs; final RxBool submitReady = false.obs;
final RxBool trustThisDevice = false.obs;
dialogManager.dismissAll(); dialogManager.dismissAll();
dialogManager.show((setState, close, context) { dialogManager.show((setState, close, context) {
@@ -1830,7 +1916,7 @@ void enter2FaDialog(
} }
submit() { submit() {
gFFI.send2FA(sessionId, controller.text.trim()); gFFI.send2FA(sessionId, controller.text.trim(), trustThisDevice.value);
close(); close();
dialogManager.showLoading(translate('Logging in...'), dialogManager.showLoading(translate('Logging in...'),
onCancel: closeConnection); onCancel: closeConnection);
@@ -1844,9 +1930,27 @@ void enter2FaDialog(
onChanged: () => submitReady.value = codeField.isReady, onChanged: () => submitReady.value = codeField.isReady,
); );
final trustField = Obx(() => CheckboxListTile(
contentPadding: const EdgeInsets.all(0),
dense: true,
controlAffinity: ListTileControlAffinity.leading,
title: Text(translate("Trust this device")),
value: trustThisDevice.value,
onChanged: (value) {
if (value == null) return;
trustThisDevice.value = value;
},
));
return CustomAlertDialog( return CustomAlertDialog(
title: Text(translate('enter-2fa-title')), title: Text(translate('enter-2fa-title')),
content: codeField, content: Column(
children: [
codeField,
if (bind.sessionGetEnableTrustedDevices(sessionId: sessionId))
trustField,
],
),
actions: [ actions: [
dialogButton('Cancel', dialogButton('Cancel',
onPressed: cancel, onPressed: cancel,
@@ -1909,3 +2013,487 @@ void showWindowsSessionsDialog(
); );
}); });
} }
void addPeersToAbDialog(
List<Peer> peers,
) async {
Future<bool> addTo(String abname) async {
final mapList = peers.map((e) {
var json = e.toJson();
// remove password when add to another address book to avoid re-share
json.remove('password');
json.remove('hash');
return json;
}).toList();
final errMsg = await gFFI.abModel.addPeersTo(mapList, abname);
if (errMsg == null) {
showToast(translate('Successful'));
return true;
} else {
BotToast.showText(text: errMsg, contentColor: Colors.red);
return false;
}
}
// if only one address book and it is personal, add to it directly
if (gFFI.abModel.addressbooks.length == 1 &&
gFFI.abModel.current.isPersonal()) {
await addTo(gFFI.abModel.currentName.value);
return;
}
RxBool isInProgress = false.obs;
final names = gFFI.abModel.addressBooksCanWrite();
RxString currentName = gFFI.abModel.currentName.value.obs;
TextEditingController controller = TextEditingController();
if (gFFI.peerTabModel.currentTab == PeerTabIndex.ab.index) {
names.remove(currentName.value);
}
if (names.isEmpty) {
debugPrint('no address book to add peers to, should not happen');
return;
}
if (!names.contains(currentName.value)) {
currentName.value = names[0];
}
gFFI.dialogManager.show((setState, close, context) {
submit() async {
if (controller.text != gFFI.abModel.translatedName(currentName.value)) {
BotToast.showText(
text: 'illegal address book name: ${controller.text}',
contentColor: Colors.red);
return;
}
isInProgress.value = true;
if (await addTo(currentName.value)) {
close();
}
isInProgress.value = false;
}
cancel() {
close();
}
return CustomAlertDialog(
title: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(IconFont.addressBook, color: MyTheme.accent),
Text(translate('Add to address book')).paddingOnly(left: 10),
],
),
content: Obx(() => Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// https://github.com/flutter/flutter/issues/145081
DropdownMenu(
initialSelection: currentName.value,
onSelected: (value) {
if (value != null) {
currentName.value = value;
}
},
dropdownMenuEntries: names
.map((e) => DropdownMenuEntry(
value: e, label: gFFI.abModel.translatedName(e)))
.toList(),
inputDecorationTheme: InputDecorationTheme(
isDense: true, border: UnderlineInputBorder()),
enableFilter: true,
controller: controller,
),
// NOT use Offstage to wrap LinearProgressIndicator
isInProgress.value ? const LinearProgressIndicator() : Offstage()
],
)),
actions: [
dialogButton(
"Cancel",
icon: Icon(Icons.close_rounded),
onPressed: cancel,
isOutline: true,
),
dialogButton(
"OK",
icon: Icon(Icons.done_rounded),
onPressed: submit,
),
],
onSubmit: submit,
onCancel: cancel,
);
});
}
void setSharedAbPasswordDialog(String abName, Peer peer) {
TextEditingController controller = TextEditingController(text: '');
RxBool isInProgress = false.obs;
RxBool isInputEmpty = true.obs;
bool passwordVisible = false;
controller.addListener(() {
isInputEmpty.value = controller.text.isEmpty;
});
gFFI.dialogManager.show((setState, close, context) {
change(String password) async {
isInProgress.value = true;
bool res =
await gFFI.abModel.changeSharedPassword(abName, peer.id, password);
isInProgress.value = false;
if (res) {
showToast(translate('Successful'));
}
close();
}
cancel() {
close();
}
return CustomAlertDialog(
title: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.key, color: MyTheme.accent),
Text(translate(peer.password.isEmpty
? 'Set shared password'
: 'Change Password'))
.paddingOnly(left: 10),
],
),
content: Obx(() => Column(children: [
TextField(
controller: controller,
autofocus: true,
obscureText: !passwordVisible,
decoration: InputDecoration(
suffixIcon: IconButton(
icon: Icon(
passwordVisible ? Icons.visibility : Icons.visibility_off,
color: MyTheme.lightTheme.primaryColor),
onPressed: () {
setState(() {
passwordVisible = !passwordVisible;
});
},
),
),
),
if (!gFFI.abModel.current.isPersonal())
Row(children: [
Icon(Icons.info, color: Colors.amber).marginOnly(right: 4),
Text(
translate('share_warning_tip'),
style: TextStyle(fontSize: 12),
)
]).marginSymmetric(vertical: 10),
// NOT use Offstage to wrap LinearProgressIndicator
isInProgress.value ? const LinearProgressIndicator() : Offstage()
])),
actions: [
dialogButton(
"Cancel",
icon: Icon(Icons.close_rounded),
onPressed: cancel,
isOutline: true,
),
if (peer.password.isNotEmpty)
dialogButton(
"Remove",
icon: Icon(Icons.delete_outline_rounded),
onPressed: () => change(''),
buttonStyle: ButtonStyle(
backgroundColor: MaterialStatePropertyAll(Colors.red)),
),
Obx(() => dialogButton(
"OK",
icon: Icon(Icons.done_rounded),
onPressed:
isInputEmpty.value ? null : () => change(controller.text),
)),
],
onSubmit: isInputEmpty.value ? null : () => change(controller.text),
onCancel: cancel,
);
});
}
void CommonConfirmDialog(OverlayDialogManager dialogManager, String content,
VoidCallback onConfirm) {
dialogManager.show((setState, close, context) {
submit() {
close();
onConfirm.call();
}
return CustomAlertDialog(
content: Row(
children: [
Expanded(
child: Text(content,
style: const TextStyle(fontSize: 15),
textAlign: TextAlign.start),
),
],
).marginOnly(bottom: 12),
actions: [
dialogButton(translate("Cancel"), onPressed: close, isOutline: true),
dialogButton(translate("OK"), onPressed: submit),
],
onSubmit: submit,
onCancel: close,
);
});
}
void changeUnlockPinDialog(String oldPin, Function() callback) {
final pinController = TextEditingController(text: oldPin);
final confirmController = TextEditingController(text: oldPin);
String? pinErrorText;
String? confirmationErrorText;
final maxLength = bind.mainMaxEncryptLen();
gFFI.dialogManager.show((setState, close, context) {
submit() async {
pinErrorText = null;
confirmationErrorText = null;
final pin = pinController.text.trim();
final confirm = confirmController.text.trim();
if (pin != confirm) {
setState(() {
confirmationErrorText =
translate('The confirmation is not identical.');
});
return;
}
final errorMsg = bind.mainSetUnlockPin(pin: pin);
if (errorMsg != '') {
setState(() {
pinErrorText = translate(errorMsg);
});
return;
}
callback.call();
close();
}
return CustomAlertDialog(
title: Text(translate("Set PIN")),
content: Column(
children: [
DialogTextField(
title: 'PIN',
controller: pinController,
obscureText: true,
errorText: pinErrorText,
maxLength: maxLength,
),
DialogTextField(
title: translate('Confirmation'),
controller: confirmController,
obscureText: true,
errorText: confirmationErrorText,
maxLength: maxLength,
)
],
).marginOnly(bottom: 12),
actions: [
dialogButton(translate("Cancel"), onPressed: close, isOutline: true),
dialogButton(translate("OK"), onPressed: submit),
],
onSubmit: submit,
onCancel: close,
);
});
}
void checkUnlockPinDialog(String correctPin, Function() passCallback) {
final controller = TextEditingController();
String? errorText;
gFFI.dialogManager.show((setState, close, context) {
submit() async {
final pin = controller.text.trim();
if (correctPin != pin) {
setState(() {
errorText = translate('Wrong PIN');
});
return;
}
passCallback.call();
close();
}
return CustomAlertDialog(
content: Row(
children: [
Expanded(
child: PasswordWidget(
title: 'PIN',
controller: controller,
errorText: errorText,
hintText: '',
))
],
).marginOnly(bottom: 12),
actions: [
dialogButton(translate("Cancel"), onPressed: close, isOutline: true),
dialogButton(translate("OK"), onPressed: submit),
],
onSubmit: submit,
onCancel: close,
);
});
}
void confrimDeleteTrustedDevicesDialog(
RxList<TrustedDevice> trustedDevices, RxList<Uint8List> selectedDevices) {
CommonConfirmDialog(gFFI.dialogManager, '${translate('Confirm Delete')}?',
() async {
if (selectedDevices.isEmpty) return;
if (selectedDevices.length == trustedDevices.length) {
await bind.mainClearTrustedDevices();
trustedDevices.clear();
selectedDevices.clear();
} else {
final json = jsonEncode(selectedDevices.map((e) => e.toList()).toList());
await bind.mainRemoveTrustedDevices(json: json);
trustedDevices.removeWhere((element) {
return selectedDevices.contains(element.hwid);
});
selectedDevices.clear();
}
});
}
void manageTrustedDeviceDialog() async {
RxList<TrustedDevice> trustedDevices = (await TrustedDevice.get()).obs;
RxList<Uint8List> selectedDevices = RxList.empty();
gFFI.dialogManager.show((setState, close, context) {
return CustomAlertDialog(
title: Text(translate("Manage trusted devices")),
content: trustedDevicesTable(trustedDevices, selectedDevices),
actions: [
Obx(() => dialogButton(translate("Delete"),
onPressed: selectedDevices.isEmpty
? null
: () {
confrimDeleteTrustedDevicesDialog(
trustedDevices,
selectedDevices,
);
},
isOutline: false)
.marginOnly(top: 12)),
dialogButton(translate("Close"), onPressed: close, isOutline: true)
.marginOnly(top: 12),
],
onCancel: close,
);
});
}
class TrustedDevice {
late final Uint8List hwid;
late final int time;
late final String id;
late final String name;
late final String platform;
TrustedDevice.fromJson(Map<String, dynamic> json) {
final hwidList = json['hwid'] as List<dynamic>;
hwid = Uint8List.fromList(hwidList.cast<int>());
time = json['time'];
id = json['id'];
name = json['name'];
platform = json['platform'];
}
String daysRemaining() {
final expiry = time + 90 * 24 * 60 * 60 * 1000;
final remaining = expiry - DateTime.now().millisecondsSinceEpoch;
if (remaining < 0) {
return '0';
}
return (remaining / (24 * 60 * 60 * 1000)).toStringAsFixed(0);
}
static Future<List<TrustedDevice>> get() async {
final List<TrustedDevice> devices = List.empty(growable: true);
try {
final devicesJson = await bind.mainGetTrustedDevices();
if (devicesJson.isNotEmpty) {
final devicesList = json.decode(devicesJson);
if (devicesList is List) {
for (var device in devicesList) {
devices.add(TrustedDevice.fromJson(device));
}
}
}
} catch (e) {
print(e.toString());
}
devices.sort((a, b) => b.time.compareTo(a.time));
return devices;
}
}
Widget trustedDevicesTable(
RxList<TrustedDevice> devices, RxList<Uint8List> selectedDevices) {
RxBool selectAll = false.obs;
setSelectAll() {
if (selectedDevices.isNotEmpty &&
selectedDevices.length == devices.length) {
selectAll.value = true;
} else {
selectAll.value = false;
}
}
devices.listen((_) {
setSelectAll();
});
selectedDevices.listen((_) {
setSelectAll();
});
return FittedBox(
child: Obx(() => DataTable(
columns: [
DataColumn(
label: Checkbox(
value: selectAll.value,
onChanged: (value) {
if (value == true) {
selectedDevices.clear();
selectedDevices.addAll(devices.map((e) => e.hwid));
} else {
selectedDevices.clear();
}
},
)),
DataColumn(label: Text(translate('Platform'))),
DataColumn(label: Text(translate('ID'))),
DataColumn(label: Text(translate('Username'))),
DataColumn(label: Text(translate('Days remaining'))),
],
rows: devices.map((device) {
return DataRow(cells: [
DataCell(Checkbox(
value: selectedDevices.contains(device.hwid),
onChanged: (value) {
if (value == null) return;
if (value) {
selectedDevices.remove(device.hwid);
selectedDevices.add(device.hwid);
} else {
selectedDevices.remove(device.hwid);
}
},
)),
DataCell(Text(device.platform)),
DataCell(Text(device.id)),
DataCell(Text(device.name)),
DataCell(Text(device.daysRemaining())),
]);
}).toList(),
)),
);
}

View File

@@ -112,6 +112,8 @@ class CustomTouchGestureRecognizer extends ScaleGestureRecognizer {
}; };
} }
// FIXME: This debounce logic is not working properly.
// If we move our finger very fast, we won't be able to detect the "oneFingerPan" event sometimes.
void onOneFingerStartDebounce(ScaleUpdateDetails d) { void onOneFingerStartDebounce(ScaleUpdateDetails d) {
start(ScaleUpdateDetails d) { start(ScaleUpdateDetails d) {
_currentState = GestureState.oneFingerPan; _currentState = GestureState.oneFingerPan;

View File

@@ -142,11 +142,6 @@ class _WidgetOPState extends State<WidgetOP> {
String _failedMsg = ''; String _failedMsg = '';
String _url = ''; String _url = '';
@override
void initState() {
super.initState();
}
@override @override
void dispose() { void dispose() {
super.dispose(); super.dispose();
@@ -455,7 +450,7 @@ Future<bool?> loginDialog() async {
} }
if (isEmailVerification != null) { if (isEmailVerification != null) {
if (isMobile) { if (isMobile) {
if (close != null) close(false); if (close != null) close(null);
verificationCodeDialog( verificationCodeDialog(
resp.user, resp.secret, isEmailVerification); resp.user, resp.secret, isEmailVerification);
} else { } else {
@@ -712,6 +707,11 @@ Future<bool?> verificationCodeDialog(
dialogButton("Verify", onPressed: getOnSubmit()), dialogButton("Verify", onPressed: getOnSubmit()),
]); ]);
}); });
// For verification code, desktop update other models in login dialog, mobile need to close login dialog first,
// otherwise the soft keyboard will jump out on each key press, so mobile update in verification code dialog.
if (isMobile && res == true) {
await UserModel.updateOtherModels();
}
return res; return res;
} }

View File

@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_hbb/common/hbbs/hbbs.dart'; import 'package:flutter_hbb/common/hbbs/hbbs.dart';
import 'package:flutter_hbb/common/widgets/login.dart'; import 'package:flutter_hbb/common/widgets/login.dart';
import 'package:flutter_hbb/common/widgets/peers_view.dart'; import 'package:flutter_hbb/common/widgets/peers_view.dart';
import 'package:flutter_hbb/models/state_model.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import '../../common.dart'; import '../../common.dart';
@@ -23,11 +24,6 @@ class _MyGroupState extends State<MyGroup> {
RxString get searchUserText => gFFI.groupModel.searchUserText; RxString get searchUserText => gFFI.groupModel.searchUserText;
static TextEditingController searchUserController = TextEditingController(); static TextEditingController searchUserController = TextEditingController();
@override
void initState() {
super.initState();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Obx(() { return Obx(() {
@@ -35,6 +31,8 @@ class _MyGroupState extends State<MyGroup> {
return Center( return Center(
child: ElevatedButton( child: ElevatedButton(
onPressed: loginDialog, child: Text(translate("Login")))); onPressed: loginDialog, child: Text(translate("Login"))));
} else if (gFFI.userModel.networkError.isNotEmpty) {
return netWorkErrorWidget();
} else if (gFFI.groupModel.groupLoading.value && gFFI.groupModel.emtpy) { } else if (gFFI.groupModel.groupLoading.value && gFFI.groupModel.emtpy) {
return const Center( return const Center(
child: CircularProgressIndicator(), child: CircularProgressIndicator(),
@@ -47,13 +45,16 @@ class _MyGroupState extends State<MyGroup> {
err: gFFI.groupModel.groupLoadError, err: gFFI.groupModel.groupLoadError,
retry: null, retry: null,
close: () => gFFI.groupModel.groupLoadError.value = ''), close: () => gFFI.groupModel.groupLoadError.value = ''),
Expanded(child: isDesktop ? _buildDesktop() : _buildMobile()) Expanded(
child: Obx(() => stateGlobal.isPortrait.isTrue
? _buildPortrait()
: _buildLandscape())),
], ],
); );
}); });
} }
Widget _buildDesktop() { Widget _buildLandscape() {
return Row( return Row(
children: [ children: [
Container( Container(
@@ -82,14 +83,14 @@ class _MyGroupState extends State<MyGroup> {
child: Align( child: Align(
alignment: Alignment.topLeft, alignment: Alignment.topLeft,
child: MyGroupPeerView( child: MyGroupPeerView(
menuPadding: widget.menuPadding, menuPadding: widget.menuPadding,
initPeers: gFFI.groupModel.peers)), )),
) )
], ],
); );
} }
Widget _buildMobile() { Widget _buildPortrait() {
return Column( return Column(
children: [ children: [
Container( Container(
@@ -114,8 +115,8 @@ class _MyGroupState extends State<MyGroup> {
child: Align( child: Align(
alignment: Alignment.topLeft, alignment: Alignment.topLeft,
child: MyGroupPeerView( child: MyGroupPeerView(
menuPadding: widget.menuPadding, menuPadding: widget.menuPadding,
initPeers: gFFI.groupModel.peers)), )),
) )
], ],
); );
@@ -159,14 +160,14 @@ class _MyGroupState extends State<MyGroup> {
} }
return true; return true;
}).toList(); }).toList();
final listView = ListView.builder( listView(bool isPortrait) => ListView.builder(
shrinkWrap: isMobile, shrinkWrap: isPortrait,
itemCount: items.length, itemCount: items.length,
itemBuilder: (context, index) => _buildUserItem(items[index])); itemBuilder: (context, index) => _buildUserItem(items[index]));
var maxHeight = max(MediaQuery.of(context).size.height / 6, 100.0); var maxHeight = max(MediaQuery.of(context).size.height / 6, 100.0);
return isDesktop return Obx(() => stateGlobal.isPortrait.isFalse
? listView ? listView(false)
: LimitedBox(maxHeight: maxHeight, child: listView); : LimitedBox(maxHeight: maxHeight, child: listView(true)));
}); });
} }

View File

@@ -1,6 +1,8 @@
import 'package:auto_size_text/auto_size_text.dart'; import 'package:auto_size_text/auto_size_text.dart';
import 'package:debounce_throttle/debounce_throttle.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/models/platform_model.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@@ -26,9 +28,12 @@ class DraggableChatWindow extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (draggablePositions.chatWindow.isInvalid()) {
draggablePositions.chatWindow.update(position);
}
return isIOS return isIOS
? IOSDraggable( ? IOSDraggable(
position: position, position: draggablePositions.chatWindow,
chatModel: chatModel, chatModel: chatModel,
width: width, width: width,
height: height, height: height,
@@ -45,7 +50,7 @@ class DraggableChatWindow extends StatelessWidget {
) )
: Draggable( : Draggable(
checkKeyboard: true, checkKeyboard: true,
position: position, position: draggablePositions.chatWindow,
width: width, width: width,
height: height, height: height,
chatModel: chatModel, chatModel: chatModel,
@@ -54,7 +59,7 @@ class DraggableChatWindow extends StatelessWidget {
resizeToAvoidBottomInset: false, resizeToAvoidBottomInset: false,
appBar: CustomAppBar( appBar: CustomAppBar(
onPanUpdate: onPanUpdate, onPanUpdate: onPanUpdate,
appBar: isDesktop appBar: (isDesktop || isWebDesktop)
? _buildDesktopAppBar(context) ? _buildDesktopAppBar(context)
: _buildMobileAppBar(context), : _buildMobileAppBar(context),
), ),
@@ -166,15 +171,17 @@ class CustomAppBar extends StatelessWidget implements PreferredSizeWidget {
/// floating buttons of back/home/recent actions for android /// floating buttons of back/home/recent actions for android
class DraggableMobileActions extends StatelessWidget { class DraggableMobileActions extends StatelessWidget {
DraggableMobileActions( DraggableMobileActions(
{this.position = Offset.zero, {this.onBackPressed,
this.onBackPressed,
this.onRecentPressed, this.onRecentPressed,
this.onHomePressed, this.onHomePressed,
this.onHidePressed, this.onHidePressed,
required this.position,
required this.width, required this.width,
required this.height}); required this.height,
required this.scale});
final Offset position; final double scale;
final DraggableKeyPosition position;
final double width; final double width;
final double height; final double height;
final VoidCallback? onBackPressed; final VoidCallback? onBackPressed;
@@ -186,8 +193,8 @@ class DraggableMobileActions extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Draggable( return Draggable(
position: position, position: position,
width: width, width: scale * width,
height: height, height: scale * height,
builder: (_, onPanUpdate) { builder: (_, onPanUpdate) {
return GestureDetector( return GestureDetector(
onPanUpdate: onPanUpdate, onPanUpdate: onPanUpdate,
@@ -197,7 +204,8 @@ class DraggableMobileActions extends StatelessWidget {
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: MyTheme.accent.withOpacity(0.4), color: MyTheme.accent.withOpacity(0.4),
borderRadius: BorderRadius.all(Radius.circular(15))), borderRadius:
BorderRadius.all(Radius.circular(15 * scale))),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround, mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [ children: [
@@ -205,17 +213,20 @@ class DraggableMobileActions extends StatelessWidget {
color: Colors.white, color: Colors.white,
onPressed: onBackPressed, onPressed: onBackPressed,
splashRadius: kDesktopIconButtonSplashRadius, splashRadius: kDesktopIconButtonSplashRadius,
icon: const Icon(Icons.arrow_back)), icon: const Icon(Icons.arrow_back),
iconSize: 24 * scale),
IconButton( IconButton(
color: Colors.white, color: Colors.white,
onPressed: onHomePressed, onPressed: onHomePressed,
splashRadius: kDesktopIconButtonSplashRadius, splashRadius: kDesktopIconButtonSplashRadius,
icon: const Icon(Icons.home)), icon: const Icon(Icons.home),
iconSize: 24 * scale),
IconButton( IconButton(
color: Colors.white, color: Colors.white,
onPressed: onRecentPressed, onPressed: onRecentPressed,
splashRadius: kDesktopIconButtonSplashRadius, splashRadius: kDesktopIconButtonSplashRadius,
icon: const Icon(Icons.more_horiz)), icon: const Icon(Icons.more_horiz),
iconSize: 24 * scale),
const VerticalDivider( const VerticalDivider(
width: 0, width: 0,
thickness: 2, thickness: 2,
@@ -226,7 +237,8 @@ class DraggableMobileActions extends StatelessWidget {
color: Colors.white, color: Colors.white,
onPressed: onHidePressed, onPressed: onHidePressed,
splashRadius: kDesktopIconButtonSplashRadius, splashRadius: kDesktopIconButtonSplashRadius,
icon: const Icon(Icons.keyboard_arrow_down)), icon: const Icon(Icons.keyboard_arrow_down),
iconSize: 24 * scale),
], ],
), ),
))); )));
@@ -234,12 +246,98 @@ class DraggableMobileActions extends StatelessWidget {
} }
} }
class DraggableKeyPosition {
final String key;
Offset _pos;
late Debouncer<int> _debouncerStore;
DraggableKeyPosition(this.key)
: _pos = DraggablePositions.kInvalidDraggablePosition;
get pos => _pos;
_loadPosition(String k) {
final value = bind.getLocalFlutterOption(k: k);
if (value.isNotEmpty) {
final parts = value.split(',');
if (parts.length == 2) {
return Offset(double.parse(parts[0]), double.parse(parts[1]));
}
}
return DraggablePositions.kInvalidDraggablePosition;
}
load() {
_pos = _loadPosition(key);
_debouncerStore = Debouncer<int>(const Duration(milliseconds: 500),
onChanged: (v) => _store(), initialValue: 0);
}
update(Offset pos) {
_pos = pos;
_triggerStore();
}
// Adjust position to keep it in the screen
// Only used for desktop and web desktop
tryAdjust(double w, double h, double scale) {
final size = MediaQuery.of(Get.context!).size;
w = w * scale;
h = h * scale;
double x = _pos.dx;
double y = _pos.dy;
if (x + w > size.width) {
x = size.width - w;
}
final tabBarHeight = isDesktop ? kDesktopRemoteTabBarHeight : 0;
if (y + h > (size.height - tabBarHeight)) {
y = size.height - tabBarHeight - h;
}
if (x < 0) {
x = 0;
}
if (y < 0) {
y = 0;
}
if (x != _pos.dx || y != _pos.dy) {
update(Offset(x, y));
}
}
isInvalid() {
return _pos == DraggablePositions.kInvalidDraggablePosition;
}
_triggerStore() => _debouncerStore.value = _debouncerStore.value + 1;
_store() {
bind.setLocalFlutterOption(k: key, v: '${_pos.dx},${_pos.dy}');
}
}
class DraggablePositions {
static const kChatWindow = 'draggablePositionChat';
static const kMobileActions = 'draggablePositionMobile';
static const kIOSDraggable = 'draggablePositionIOS';
static const kInvalidDraggablePosition = Offset(-999999, -999999);
final chatWindow = DraggableKeyPosition(kChatWindow);
final mobileActions = DraggableKeyPosition(kMobileActions);
final iOSDraggable = DraggableKeyPosition(kIOSDraggable);
load() {
chatWindow.load();
mobileActions.load();
iOSDraggable.load();
}
}
DraggablePositions draggablePositions = DraggablePositions();
class Draggable extends StatefulWidget { class Draggable extends StatefulWidget {
const Draggable( Draggable(
{Key? key, {Key? key,
this.checkKeyboard = false, this.checkKeyboard = false,
this.checkScreenSize = false, this.checkScreenSize = false,
this.position = Offset.zero, required this.position,
required this.width, required this.width,
required this.height, required this.height,
this.chatModel, this.chatModel,
@@ -248,55 +346,53 @@ class Draggable extends StatefulWidget {
final bool checkKeyboard; final bool checkKeyboard;
final bool checkScreenSize; final bool checkScreenSize;
final Offset position; final DraggableKeyPosition position;
final double width; final double width;
final double height; final double height;
final ChatModel? chatModel; final ChatModel? chatModel;
final Widget Function(BuildContext, GestureDragUpdateCallback) builder; final Widget Function(BuildContext, GestureDragUpdateCallback) builder;
@override @override
State<StatefulWidget> createState() => _DraggableState(); State<StatefulWidget> createState() => _DraggableState(chatModel);
} }
class _DraggableState extends State<Draggable> { class _DraggableState extends State<Draggable> {
late Offset _position;
late ChatModel? _chatModel; late ChatModel? _chatModel;
bool _keyboardVisible = false; bool _keyboardVisible = false;
double _saveHeight = 0; double _saveHeight = 0;
double _lastBottomHeight = 0; double _lastBottomHeight = 0;
@override _DraggableState(ChatModel? chatModel) {
void initState() { _chatModel = chatModel;
super.initState();
_position = widget.position;
_chatModel = widget.chatModel;
} }
get position => widget.position.pos;
void onPanUpdate(DragUpdateDetails d) { void onPanUpdate(DragUpdateDetails d) {
final offset = d.delta; final offset = d.delta;
final size = MediaQuery.of(context).size; final size = MediaQuery.of(context).size;
double x = 0; double x = 0;
double y = 0; double y = 0;
if (_position.dx + offset.dx + widget.width > size.width) { if (position.dx + offset.dx + widget.width > size.width) {
x = size.width - widget.width; x = size.width - widget.width;
} else if (_position.dx + offset.dx < 0) { } else if (position.dx + offset.dx < 0) {
x = 0; x = 0;
} else { } else {
x = _position.dx + offset.dx; x = position.dx + offset.dx;
} }
if (_position.dy + offset.dy + widget.height > size.height) { if (position.dy + offset.dy + widget.height > size.height) {
y = size.height - widget.height; y = size.height - widget.height;
} else if (_position.dy + offset.dy < 0) { } else if (position.dy + offset.dy < 0) {
y = 0; y = 0;
} else { } else {
y = _position.dy + offset.dy; y = position.dy + offset.dy;
} }
setState(() { setState(() {
_position = Offset(x, y); widget.position.update(Offset(x, y));
}); });
_chatModel?.setChatWindowPosition(_position); _chatModel?.setChatWindowPosition(position);
} }
checkScreenSize() {} checkScreenSize() {}
@@ -307,13 +403,13 @@ class _DraggableState extends State<Draggable> {
// save // save
if (!_keyboardVisible && currentVisible) { if (!_keyboardVisible && currentVisible) {
_saveHeight = _position.dy; _saveHeight = position.dy;
} }
// reset // reset
if (_lastBottomHeight > 0 && bottomHeight == 0) { if (_lastBottomHeight > 0 && bottomHeight == 0) {
setState(() { setState(() {
_position = Offset(_position.dx, _saveHeight); widget.position.update(Offset(position.dx, _saveHeight));
}); });
} }
@@ -321,10 +417,10 @@ class _DraggableState extends State<Draggable> {
if (_keyboardVisible && currentVisible) { if (_keyboardVisible && currentVisible) {
final sumHeight = bottomHeight + widget.height; final sumHeight = bottomHeight + widget.height;
final contextHeight = MediaQuery.of(context).size.height; final contextHeight = MediaQuery.of(context).size.height;
if (sumHeight + _position.dy > contextHeight) { if (sumHeight + position.dy > contextHeight) {
final y = contextHeight - sumHeight; final y = contextHeight - sumHeight;
setState(() { setState(() {
_position = Offset(_position.dx, y); widget.position.update(Offset(position.dx, y));
}); });
} }
} }
@@ -343,8 +439,8 @@ class _DraggableState extends State<Draggable> {
} }
return Stack(children: [ return Stack(children: [
Positioned( Positioned(
top: _position.dy, top: position.dy,
left: _position.dx, left: position.dx,
width: widget.width, width: widget.width,
height: widget.height, height: widget.height,
child: widget.builder(context, onPanUpdate)) child: widget.builder(context, onPanUpdate))
@@ -355,25 +451,25 @@ class _DraggableState extends State<Draggable> {
class IOSDraggable extends StatefulWidget { class IOSDraggable extends StatefulWidget {
const IOSDraggable( const IOSDraggable(
{Key? key, {Key? key,
this.position = Offset.zero,
this.chatModel, this.chatModel,
required this.position,
required this.width, required this.width,
required this.height, required this.height,
required this.builder}) required this.builder})
: super(key: key); : super(key: key);
final Offset position; final DraggableKeyPosition position;
final ChatModel? chatModel; final ChatModel? chatModel;
final double width; final double width;
final double height; final double height;
final Widget Function(BuildContext) builder; final Widget Function(BuildContext) builder;
@override @override
IOSDraggableState createState() => IOSDraggableState(); IOSDraggableState createState() =>
IOSDraggableState(chatModel, width, height);
} }
class IOSDraggableState extends State<IOSDraggable> { class IOSDraggableState extends State<IOSDraggable> {
late Offset _position;
late ChatModel? _chatModel; late ChatModel? _chatModel;
late double _width; late double _width;
late double _height; late double _height;
@@ -381,28 +477,27 @@ class IOSDraggableState extends State<IOSDraggable> {
double _saveHeight = 0; double _saveHeight = 0;
double _lastBottomHeight = 0; double _lastBottomHeight = 0;
@override IOSDraggableState(ChatModel? chatModel, double w, double h) {
void initState() { _chatModel = chatModel;
super.initState(); _width = w;
_position = widget.position; _height = h;
_chatModel = widget.chatModel;
_width = widget.width;
_height = widget.height;
} }
DraggableKeyPosition get position => widget.position;
checkKeyboard() { checkKeyboard() {
final bottomHeight = MediaQuery.of(context).viewInsets.bottom; final bottomHeight = MediaQuery.of(context).viewInsets.bottom;
final currentVisible = bottomHeight != 0; final currentVisible = bottomHeight != 0;
// save // save
if (!_keyboardVisible && currentVisible) { if (!_keyboardVisible && currentVisible) {
_saveHeight = _position.dy; _saveHeight = position.pos.dy;
} }
// reset // reset
if (_lastBottomHeight > 0 && bottomHeight == 0) { if (_lastBottomHeight > 0 && bottomHeight == 0) {
setState(() { setState(() {
_position = Offset(_position.dx, _saveHeight); position.update(Offset(position.pos.dx, _saveHeight));
}); });
} }
@@ -410,10 +505,10 @@ class IOSDraggableState extends State<IOSDraggable> {
if (_keyboardVisible && currentVisible) { if (_keyboardVisible && currentVisible) {
final sumHeight = bottomHeight + _height; final sumHeight = bottomHeight + _height;
final contextHeight = MediaQuery.of(context).size.height; final contextHeight = MediaQuery.of(context).size.height;
if (sumHeight + _position.dy > contextHeight) { if (sumHeight + position.pos.dy > contextHeight) {
final y = contextHeight - sumHeight; final y = contextHeight - sumHeight;
setState(() { setState(() {
_position = Offset(_position.dx, y); position.update(Offset(position.pos.dx, y));
}); });
} }
} }
@@ -428,14 +523,14 @@ class IOSDraggableState extends State<IOSDraggable> {
return Stack( return Stack(
children: [ children: [
Positioned( Positioned(
left: _position.dx, left: position.pos.dx,
top: _position.dy, top: position.pos.dy,
child: GestureDetector( child: GestureDetector(
onPanUpdate: (details) { onPanUpdate: (details) {
setState(() { setState(() {
_position += details.delta; position.update(position.pos + details.delta);
}); });
_chatModel?.setChatWindowPosition(_position); _chatModel?.setChatWindowPosition(position.pos);
}, },
child: Material( child: Material(
child: Container( child: Container(
@@ -491,8 +586,10 @@ class QualityMonitor extends StatelessWidget {
children: [ children: [
_row("Speed", qualityMonitorModel.data.speed ?? '-'), _row("Speed", qualityMonitorModel.data.speed ?? '-'),
_row("FPS", qualityMonitorModel.data.fps ?? '-'), _row("FPS", qualityMonitorModel.data.fps ?? '-'),
// let delay be 0 if fps is 0
_row( _row(
"Delay", "${qualityMonitorModel.data.delay ?? '-'}ms", "Delay",
"${qualityMonitorModel.data.delay == null ? '-' : (qualityMonitorModel.data.fps ?? "").replaceAll(' ', '').replaceAll('0', '').isEmpty ? 0 : qualityMonitorModel.data.delay}ms",
rightColor: Colors.green), rightColor: Colors.green),
_row("Target Bitrate", _row("Target Bitrate",
"${qualityMonitorModel.data.targetBitrate ?? '-'}kb"), "${qualityMonitorModel.data.targetBitrate ?? '-'}kb"),

View File

@@ -1,10 +1,10 @@
import 'dart:io'; import 'package:bot_toast/bot_toast.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_hbb/common/widgets/dialog.dart'; import 'package:flutter_hbb/common/widgets/dialog.dart';
import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/models/peer_tab_model.dart'; import 'package:flutter_hbb/models/peer_tab_model.dart';
import 'package:flutter_hbb/models/state_model.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@@ -23,6 +23,8 @@ enum PeerUiType { grid, tile, list }
final peerCardUiType = PeerUiType.grid.obs; final peerCardUiType = PeerUiType.grid.obs;
bool? hideUsernameOnCard;
class _PeerCard extends StatefulWidget { class _PeerCard extends StatefulWidget {
final Peer peer; final Peer peer;
final PeerTabIndex tab; final PeerTabIndex tab;
@@ -52,42 +54,44 @@ class _PeerCardState extends State<_PeerCard>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
super.build(context); super.build(context);
if (isDesktop) { return Obx(() =>
return _buildDesktop(); stateGlobal.isPortrait.isTrue ? _buildPortrait() : _buildLandscape());
} else {
return _buildMobile();
}
} }
Widget _buildMobile() { Widget gestureDetector({required Widget child}) {
final peer = super.widget.peer;
final PeerTabModel peerTabModel = Provider.of(context); final PeerTabModel peerTabModel = Provider.of(context);
final peer = super.widget.peer;
return GestureDetector(
onDoubleTap: peerTabModel.multiSelectionMode
? null
: () => widget.connect(context, peer.id),
onTap: () {
if (peerTabModel.multiSelectionMode) {
peerTabModel.select(peer);
} else {
if (isMobile) {
widget.connect(context, peer.id);
} else {
peerTabModel.select(peer);
}
}
},
onLongPress: () => peerTabModel.select(peer),
child: child);
}
Widget _buildPortrait() {
final peer = super.widget.peer;
return Card( return Card(
margin: EdgeInsets.symmetric(horizontal: 2), margin: EdgeInsets.symmetric(horizontal: 2),
child: GestureDetector( child: gestureDetector(
onTap: () {
if (peerTabModel.multiSelectionMode) {
peerTabModel.select(peer);
} else {
if (!isWebDesktop) {
connectInPeerTab(context, peer.id, widget.tab);
}
}
},
onDoubleTap: isWebDesktop
? () => connectInPeerTab(context, peer.id, widget.tab)
: null,
onLongPress: () {
peerTabModel.select(peer);
},
child: Container( child: Container(
padding: EdgeInsets.only(left: 12, top: 8, bottom: 8), padding: EdgeInsets.only(left: 12, top: 8, bottom: 8),
child: _buildPeerTile(context, peer, null)), child: _buildPeerTile(context, peer, null)),
)); ));
} }
Widget _buildDesktop() { Widget _buildLandscape() {
final PeerTabModel peerTabModel = Provider.of(context);
final peer = super.widget.peer; final peer = super.widget.peer;
var deco = Rx<BoxDecoration?>( var deco = Rx<BoxDecoration?>(
BoxDecoration( BoxDecoration(
@@ -116,45 +120,48 @@ class _PeerCardState extends State<_PeerCard>
), ),
); );
}, },
child: GestureDetector( child: gestureDetector(
onDoubleTap:
peerTabModel.multiSelectionMode || peerTabModel.isShiftDown
? null
: () => widget.connect(context, peer.id),
onTap: () => peerTabModel.select(peer),
onLongPress: () => peerTabModel.select(peer),
child: Obx(() => peerCardUiType.value == PeerUiType.grid child: Obx(() => peerCardUiType.value == PeerUiType.grid
? _buildPeerCard(context, peer, deco) ? _buildPeerCard(context, peer, deco)
: _buildPeerTile(context, peer, deco))), : _buildPeerTile(context, peer, deco))),
); );
} }
Widget _buildPeerTile( makeChild(bool isPortrait, Peer peer) {
BuildContext context, Peer peer, Rx<BoxDecoration?>? deco) { final name = hideUsernameOnCard == true
final name = ? peer.hostname
'${peer.username}${peer.username.isNotEmpty && peer.hostname.isNotEmpty ? '@' : ''}${peer.hostname}'; : '${peer.username}${peer.username.isNotEmpty && peer.hostname.isNotEmpty ? '@' : ''}${peer.hostname}';
final greyStyle = TextStyle( final greyStyle = TextStyle(
fontSize: 11, fontSize: 11,
color: Theme.of(context).textTheme.titleLarge?.color?.withOpacity(0.6)); color: Theme.of(context).textTheme.titleLarge?.color?.withOpacity(0.6));
final child = Row( return Row(
mainAxisSize: MainAxisSize.max, mainAxisSize: MainAxisSize.max,
children: [ children: [
Container( Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: str2color('${peer.id}${peer.platform}', 0x7f), color: str2color('${peer.id}${peer.platform}', 0x7f),
borderRadius: isMobile borderRadius: isPortrait
? BorderRadius.circular(_tileRadius) ? BorderRadius.circular(_tileRadius)
: BorderRadius.only( : BorderRadius.only(
topLeft: Radius.circular(_tileRadius), topLeft: Radius.circular(_tileRadius),
bottomLeft: Radius.circular(_tileRadius), bottomLeft: Radius.circular(_tileRadius),
),
),
alignment: Alignment.center,
width: isPortrait ? 50 : 42,
height: isPortrait ? 50 : null,
child: Stack(
children: [
getPlatformImage(peer.platform, size: isPortrait ? 38 : 30)
.paddingAll(6),
if (_shouldBuildPasswordIcon(peer))
Positioned(
top: 1,
left: 1,
child: Icon(Icons.key, size: 6, color: Colors.white),
), ),
), ],
alignment: Alignment.center, )),
width: isMobile ? 50 : 42,
height: isMobile ? 50 : null,
child: getPlatformImage(peer.platform, size: isMobile ? 38 : 30)
.paddingAll(6),
),
Expanded( Expanded(
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
@@ -170,19 +177,19 @@ class _PeerCardState extends State<_PeerCard>
child: Column( child: Column(
children: [ children: [
Row(children: [ Row(children: [
getOnline(isMobile ? 4 : 8, peer.online), getOnline(isPortrait ? 4 : 8, peer.online),
Expanded( Expanded(
child: Text( child: Text(
peer.alias.isEmpty ? formatID(peer.id) : peer.alias, peer.alias.isEmpty ? formatID(peer.id) : peer.alias,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.titleSmall, style: Theme.of(context).textTheme.titleSmall,
)), )),
]).marginOnly(top: isMobile ? 0 : 2), ]).marginOnly(top: isPortrait ? 0 : 2),
Align( Align(
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
child: Text( child: Text(
name, name,
style: isMobile ? null : greyStyle, style: isPortrait ? null : greyStyle,
textAlign: TextAlign.start, textAlign: TextAlign.start,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
@@ -190,52 +197,65 @@ class _PeerCardState extends State<_PeerCard>
], ],
).marginOnly(top: 2), ).marginOnly(top: 2),
), ),
isMobile isPortrait
? checkBoxOrActionMoreMobile(peer) ? checkBoxOrActionMorePortrait(peer)
: checkBoxOrActionMoreDesktop(peer, isTile: true), : checkBoxOrActionMoreLandscape(peer, isTile: true),
], ],
).paddingOnly(left: 10.0, top: 3.0), ).paddingOnly(left: 10.0, top: 3.0),
), ),
) )
], ],
); );
final colors = }
_frontN(peer.tags, 25).map((e) => gFFI.abModel.getTagColor(e)).toList();
Widget _buildPeerTile(
BuildContext context, Peer peer, Rx<BoxDecoration?>? deco) {
hideUsernameOnCard ??=
bind.mainGetBuildinOption(key: kHideUsernameOnCard) == 'Y';
final colors = _frontN(peer.tags, 25)
.map((e) => gFFI.abModel.getCurrentAbTagColor(e))
.toList();
return Tooltip( return Tooltip(
message: isMobile message: !(isDesktop || isWebDesktop)
? '' ? ''
: peer.tags.isNotEmpty : peer.tags.isNotEmpty
? '${translate('Tags')}: ${peer.tags.join(', ')}' ? '${translate('Tags')}: ${peer.tags.join(', ')}'
: '', : '',
child: Stack(children: [ child: Stack(children: [
deco == null Obx(
? child () => deco == null
: Obx( ? makeChild(stateGlobal.isPortrait.isTrue, peer)
() => Container( : Container(
foregroundDecoration: deco.value, foregroundDecoration: deco.value,
child: child, child: makeChild(stateGlobal.isPortrait.isTrue, peer),
), ),
), ),
if (colors.isNotEmpty) if (colors.isNotEmpty)
Positioned( Obx(() => Positioned(
top: 2, top: 2,
right: isMobile ? 20 : 10, right: stateGlobal.isPortrait.isTrue ? 20 : 10,
child: CustomPaint( child: CustomPaint(
painter: TagPainter(radius: 3, colors: colors), painter: TagPainter(radius: 3, colors: colors),
), ),
) ))
]), ]),
); );
} }
Widget _buildPeerCard( Widget _buildPeerCard(
BuildContext context, Peer peer, Rx<BoxDecoration?> deco) { BuildContext context, Peer peer, Rx<BoxDecoration?> deco) {
final name = hideUsernameOnCard ??=
'${peer.username}${peer.username.isNotEmpty && peer.hostname.isNotEmpty ? '@' : ''}${peer.hostname}'; bind.mainGetBuildinOption(key: kHideUsernameOnCard) == 'Y';
final name = hideUsernameOnCard == true
? peer.hostname
: '${peer.username}${peer.username.isNotEmpty && peer.hostname.isNotEmpty ? '@' : ''}${peer.hostname}';
final child = Card( final child = Card(
color: Colors.transparent, color: Colors.transparent,
elevation: 0, elevation: 0,
margin: EdgeInsets.zero, margin: EdgeInsets.zero,
// to-do: memory leak here, more investigation needed.
// Continious rebuilds of `Obx()` will cause memory leak here.
// The simple demo does not have this issue.
child: Obx( child: Obx(
() => Container( () => Container(
foregroundDecoration: deco.value, foregroundDecoration: deco.value,
@@ -299,7 +319,7 @@ class _PeerCardState extends State<_PeerCard>
style: Theme.of(context).textTheme.titleSmall, style: Theme.of(context).textTheme.titleSmall,
)), )),
]).paddingSymmetric(vertical: 8)), ]).paddingSymmetric(vertical: 8)),
checkBoxOrActionMoreDesktop(peer, isTile: false), checkBoxOrActionMoreLandscape(peer, isTile: false),
], ],
).paddingSymmetric(horizontal: 12.0), ).paddingSymmetric(horizontal: 12.0),
) )
@@ -310,14 +330,21 @@ class _PeerCardState extends State<_PeerCard>
), ),
); );
final colors = final colors = _frontN(peer.tags, 25)
_frontN(peer.tags, 25).map((e) => gFFI.abModel.getTagColor(e)).toList(); .map((e) => gFFI.abModel.getCurrentAbTagColor(e))
.toList();
return Tooltip( return Tooltip(
message: peer.tags.isNotEmpty message: peer.tags.isNotEmpty
? '${translate('Tags')}: ${peer.tags.join(', ')}' ? '${translate('Tags')}: ${peer.tags.join(', ')}'
: '', : '',
child: Stack(children: [ child: Stack(children: [
child, child,
if (_shouldBuildPasswordIcon(peer))
Positioned(
top: 4,
left: 12,
child: Icon(Icons.key, size: 12, color: Colors.white),
),
if (colors.isNotEmpty) if (colors.isNotEmpty)
Positioned( Positioned(
top: 4, top: 4,
@@ -338,7 +365,7 @@ class _PeerCardState extends State<_PeerCard>
} }
} }
Widget checkBoxOrActionMoreMobile(Peer peer) { Widget checkBoxOrActionMorePortrait(Peer peer) {
final PeerTabModel peerTabModel = Provider.of(context); final PeerTabModel peerTabModel = Provider.of(context);
final selected = peerTabModel.isPeerSelected(peer.id); final selected = peerTabModel.isPeerSelected(peer.id);
if (peerTabModel.multiSelectionMode) { if (peerTabModel.multiSelectionMode) {
@@ -366,7 +393,7 @@ class _PeerCardState extends State<_PeerCard>
} }
} }
Widget checkBoxOrActionMoreDesktop(Peer peer, {required bool isTile}) { Widget checkBoxOrActionMoreLandscape(Peer peer, {required bool isTile}) {
final PeerTabModel peerTabModel = Provider.of(context); final PeerTabModel peerTabModel = Provider.of(context);
final selected = peerTabModel.isPeerSelected(peer.id); final selected = peerTabModel.isPeerSelected(peer.id);
if (peerTabModel.multiSelectionMode) { if (peerTabModel.multiSelectionMode) {
@@ -401,6 +428,12 @@ class _PeerCardState extends State<_PeerCard>
onPointerUp: (_) => _showPeerMenu(peer.id), onPointerUp: (_) => _showPeerMenu(peer.id),
child: build_more(context)); child: build_more(context));
bool _shouldBuildPasswordIcon(Peer peer) {
if (gFFI.peerTabModel.currentTab != PeerTabIndex.ab.index) return false;
if (gFFI.abModel.current.isPersonal()) return false;
return peer.password.isNotEmpty;
}
/// Show the peer menu and handle user's choice. /// Show the peer menu and handle user's choice.
/// User might remove the peer or send a file to the peer. /// User might remove the peer or send a file to the peer.
void _showPeerMenu(String id) async { void _showPeerMenu(String id) async {
@@ -431,7 +464,7 @@ abstract class BasePeerCard extends StatelessWidget {
peer: peer, peer: peer,
tab: tab, tab: tab,
connect: (BuildContext context, String id) => connect: (BuildContext context, String id) =>
connectInPeerTab(context, id, tab), connectInPeerTab(context, peer, tab),
popupMenuEntryBuilder: _buildPopupMenuEntry, popupMenuEntryBuilder: _buildPopupMenuEntry,
); );
} }
@@ -453,7 +486,6 @@ abstract class BasePeerCard extends StatelessWidget {
MenuEntryBase<String> _connectCommonAction( MenuEntryBase<String> _connectCommonAction(
BuildContext context, BuildContext context,
String id,
String title, { String title, {
bool isFileTransfer = false, bool isFileTransfer = false,
bool isTcpTunneling = false, bool isTcpTunneling = false,
@@ -467,7 +499,7 @@ abstract class BasePeerCard extends StatelessWidget {
proc: () { proc: () {
connectInPeerTab( connectInPeerTab(
context, context,
peer.id, peer,
tab, tab,
isFileTransfer: isFileTransfer, isFileTransfer: isFileTransfer,
isTcpTunneling: isTcpTunneling, isTcpTunneling: isTcpTunneling,
@@ -480,10 +512,9 @@ abstract class BasePeerCard extends StatelessWidget {
} }
@protected @protected
MenuEntryBase<String> _connectAction(BuildContext context, Peer peer) { MenuEntryBase<String> _connectAction(BuildContext context) {
return _connectCommonAction( return _connectCommonAction(
context, context,
peer.id,
(peer.alias.isEmpty (peer.alias.isEmpty
? translate('Connect') ? translate('Connect')
: '${translate('Connect')} ${peer.id}'), : '${translate('Connect')} ${peer.id}'),
@@ -491,20 +522,18 @@ abstract class BasePeerCard extends StatelessWidget {
} }
@protected @protected
MenuEntryBase<String> _transferFileAction(BuildContext context, String id) { MenuEntryBase<String> _transferFileAction(BuildContext context) {
return _connectCommonAction( return _connectCommonAction(
context, context,
id,
translate('Transfer file'), translate('Transfer file'),
isFileTransfer: true, isFileTransfer: true,
); );
} }
@protected @protected
MenuEntryBase<String> _tcpTunnelingAction(BuildContext context, String id) { MenuEntryBase<String> _tcpTunnelingAction(BuildContext context) {
return _connectCommonAction( return _connectCommonAction(
context, context,
id,
translate('TCP tunneling'), translate('TCP tunneling'),
isTcpTunneling: true, isTcpTunneling: true,
); );
@@ -541,7 +570,7 @@ abstract class BasePeerCard extends StatelessWidget {
], ],
)), )),
proc: () { proc: () {
connectInPeerTab(context, id, tab, isRDP: true); connectInPeerTab(context, peer, tab, isRDP: true);
}, },
padding: menuPadding, padding: menuPadding,
dismissOnClicked: true, dismissOnClicked: true,
@@ -610,8 +639,8 @@ abstract class BasePeerCard extends StatelessWidget {
@protected @protected
Future<bool> _isForceAlwaysRelay(String id) async { Future<bool> _isForceAlwaysRelay(String id) async {
return (await bind.mainGetPeerOption(id: id, key: kOptionForceAlwaysRelay)) return option2bool(kOptionForceAlwaysRelay,
.isNotEmpty; (await bind.mainGetPeerOption(id: id, key: kOptionForceAlwaysRelay)));
} }
@protected @protected
@@ -648,9 +677,8 @@ abstract class BasePeerCard extends StatelessWidget {
onSubmit: (String newName) async { onSubmit: (String newName) async {
if (newName != oldName) { if (newName != oldName) {
if (tab == PeerTabIndex.ab) { if (tab == PeerTabIndex.ab) {
gFFI.abModel.changeAlias(id: id, alias: newName); await gFFI.abModel.changeAlias(id: id, alias: newName);
await bind.mainSetPeerAlias(id: id, alias: newName); await bind.mainSetPeerAlias(id: id, alias: newName);
gFFI.abModel.pushAb();
} else { } else {
await bind.mainSetPeerAlias(id: id, alias: newName); await bind.mainSetPeerAlias(id: id, alias: newName);
showToast(translate('Successful')); showToast(translate('Successful'));
@@ -702,11 +730,7 @@ abstract class BasePeerCard extends StatelessWidget {
await bind.mainLoadLanPeers(); await bind.mainLoadLanPeers();
break; break;
case PeerTabIndex.ab: case PeerTabIndex.ab:
gFFI.abModel.deletePeer(id); await gFFI.abModel.deletePeers([id]);
final future = gFFI.abModel.pushAb();
if (await bind.mainPeerExists(id: peer.id)) {
gFFI.abModel.reSyncToast(future);
}
break; break;
case PeerTabIndex.group: case PeerTabIndex.group:
break; break;
@@ -716,7 +740,7 @@ abstract class BasePeerCard extends StatelessWidget {
} }
} }
deletePeerConfirmDialog(onSubmit, deleteConfirmDialog(onSubmit,
'${translate('Delete')} "${peer.alias.isEmpty ? formatID(peer.id) : peer.alias}"?'); '${translate('Delete')} "${peer.alias.isEmpty ? formatID(peer.id) : peer.alias}"?');
}, },
padding: menuPadding, padding: menuPadding,
@@ -732,14 +756,16 @@ abstract class BasePeerCard extends StatelessWidget {
style: style, style: style,
), ),
proc: () async { proc: () async {
bool result = gFFI.abModel.changePassword(id, ''); bool succ = await gFFI.abModel.changePersonalHashPassword(id, '');
await bind.mainForgetPassword(id: id); await bind.mainForgetPassword(id: id);
bool toast = false; if (succ) {
if (result) { showToast(translate('Successful'));
toast = tab == PeerTabIndex.ab; } else {
gFFI.abModel.pushAb(toastIfFail: toast, toastIfSucc: toast); if (tab.index == PeerTabIndex.ab.index) {
BotToast.showText(
contentColor: Colors.red, text: translate("Failed"));
}
} }
if (!toast) showToast(translate('Successful'));
}, },
padding: menuPadding, padding: menuPadding,
dismissOnClicked: true, dismissOnClicked: true,
@@ -824,13 +850,7 @@ abstract class BasePeerCard extends StatelessWidget {
), ),
proc: () { proc: () {
() async { () async {
if (gFFI.abModel.isFull(true)) { addPeersToAbDialog([Peer.copy(peer)]);
return;
}
if (!gFFI.abModel.idContainBy(peer.id)) {
gFFI.abModel.addPeer(peer);
gFFI.abModel.pushAb();
}
}(); }();
}, },
padding: menuPadding, padding: menuPadding,
@@ -858,25 +878,29 @@ class RecentPeerCard extends BasePeerCard {
Future<List<MenuEntryBase<String>>> _buildMenuItems( Future<List<MenuEntryBase<String>>> _buildMenuItems(
BuildContext context) async { BuildContext context) async {
final List<MenuEntryBase<String>> menuItems = [ final List<MenuEntryBase<String>> menuItems = [
_connectAction(context, peer), _connectAction(context),
_transferFileAction(context, peer.id), _transferFileAction(context),
]; ];
final List favs = (await bind.mainGetFav()).toList(); final List favs = (await bind.mainGetFav()).toList();
if (isDesktop && peer.platform != kPeerPlatformAndroid) { if (isDesktop && peer.platform != kPeerPlatformAndroid) {
menuItems.add(_tcpTunnelingAction(context, peer.id)); menuItems.add(_tcpTunnelingAction(context));
} }
// menuItems.add(await _openNewConnInOptAction(peer.id)); // menuItems.add(await _openNewConnInOptAction(peer.id));
menuItems.add(await _forceAlwaysRelayAction(peer.id)); if (!isWeb) {
if (Platform.isWindows && peer.platform == kPeerPlatformWindows) { menuItems.add(await _forceAlwaysRelayAction(peer.id));
}
if (isWindows && peer.platform == kPeerPlatformWindows) {
menuItems.add(_rdpAction(context, peer.id)); menuItems.add(_rdpAction(context, peer.id));
} }
if (Platform.isWindows) { if (isWindows) {
menuItems.add(_createShortCutAction(peer.id)); menuItems.add(_createShortCutAction(peer.id));
} }
menuItems.add(MenuEntryDivider()); menuItems.add(MenuEntryDivider());
menuItems.add(_renameAction(peer.id)); if (isMobile || isDesktop || isWebDesktop) {
menuItems.add(_renameAction(peer.id));
}
if (await bind.mainPeerHasPassword(id: peer.id)) { if (await bind.mainPeerHasPassword(id: peer.id)) {
menuItems.add(_unrememberPasswordAction(peer.id)); menuItems.add(_unrememberPasswordAction(peer.id));
} }
@@ -888,9 +912,7 @@ class RecentPeerCard extends BasePeerCard {
} }
if (gFFI.userModel.userName.isNotEmpty) { if (gFFI.userModel.userName.isNotEmpty) {
if (!gFFI.abModel.idContainBy(peer.id)) { menuItems.add(_addToAb(peer));
menuItems.add(_addToAb(peer));
}
} }
menuItems.add(MenuEntryDivider()); menuItems.add(MenuEntryDivider());
@@ -915,22 +937,26 @@ class FavoritePeerCard extends BasePeerCard {
Future<List<MenuEntryBase<String>>> _buildMenuItems( Future<List<MenuEntryBase<String>>> _buildMenuItems(
BuildContext context) async { BuildContext context) async {
final List<MenuEntryBase<String>> menuItems = [ final List<MenuEntryBase<String>> menuItems = [
_connectAction(context, peer), _connectAction(context),
_transferFileAction(context, peer.id), _transferFileAction(context),
]; ];
if (isDesktop && peer.platform != kPeerPlatformAndroid) { if (isDesktop && peer.platform != kPeerPlatformAndroid) {
menuItems.add(_tcpTunnelingAction(context, peer.id)); menuItems.add(_tcpTunnelingAction(context));
} }
// menuItems.add(await _openNewConnInOptAction(peer.id)); // menuItems.add(await _openNewConnInOptAction(peer.id));
menuItems.add(await _forceAlwaysRelayAction(peer.id)); if (!isWeb) {
if (Platform.isWindows && peer.platform == kPeerPlatformWindows) { menuItems.add(await _forceAlwaysRelayAction(peer.id));
}
if (isWindows && peer.platform == kPeerPlatformWindows) {
menuItems.add(_rdpAction(context, peer.id)); menuItems.add(_rdpAction(context, peer.id));
} }
if (Platform.isWindows) { if (isWindows) {
menuItems.add(_createShortCutAction(peer.id)); menuItems.add(_createShortCutAction(peer.id));
} }
menuItems.add(MenuEntryDivider()); menuItems.add(MenuEntryDivider());
menuItems.add(_renameAction(peer.id)); if (isMobile || isDesktop || isWebDesktop) {
menuItems.add(_renameAction(peer.id));
}
if (await bind.mainPeerHasPassword(id: peer.id)) { if (await bind.mainPeerHasPassword(id: peer.id)) {
menuItems.add(_unrememberPasswordAction(peer.id)); menuItems.add(_unrememberPasswordAction(peer.id));
} }
@@ -939,9 +965,7 @@ class FavoritePeerCard extends BasePeerCard {
})); }));
if (gFFI.userModel.userName.isNotEmpty) { if (gFFI.userModel.userName.isNotEmpty) {
if (!gFFI.abModel.idContainBy(peer.id)) { menuItems.add(_addToAb(peer));
menuItems.add(_addToAb(peer));
}
} }
menuItems.add(MenuEntryDivider()); menuItems.add(MenuEntryDivider());
@@ -966,22 +990,24 @@ class DiscoveredPeerCard extends BasePeerCard {
Future<List<MenuEntryBase<String>>> _buildMenuItems( Future<List<MenuEntryBase<String>>> _buildMenuItems(
BuildContext context) async { BuildContext context) async {
final List<MenuEntryBase<String>> menuItems = [ final List<MenuEntryBase<String>> menuItems = [
_connectAction(context, peer), _connectAction(context),
_transferFileAction(context, peer.id), _transferFileAction(context),
]; ];
final List favs = (await bind.mainGetFav()).toList(); final List favs = (await bind.mainGetFav()).toList();
if (isDesktop && peer.platform != kPeerPlatformAndroid) { if (isDesktop && peer.platform != kPeerPlatformAndroid) {
menuItems.add(_tcpTunnelingAction(context, peer.id)); menuItems.add(_tcpTunnelingAction(context));
} }
// menuItems.add(await _openNewConnInOptAction(peer.id)); // menuItems.add(await _openNewConnInOptAction(peer.id));
menuItems.add(await _forceAlwaysRelayAction(peer.id)); if (!isWeb) {
if (Platform.isWindows && peer.platform == kPeerPlatformWindows) { menuItems.add(await _forceAlwaysRelayAction(peer.id));
}
if (isWindows && peer.platform == kPeerPlatformWindows) {
menuItems.add(_rdpAction(context, peer.id)); menuItems.add(_rdpAction(context, peer.id));
} }
menuItems.add(_wolAction(peer.id)); menuItems.add(_wolAction(peer.id));
if (Platform.isWindows) { if (isWindows) {
menuItems.add(_createShortCutAction(peer.id)); menuItems.add(_createShortCutAction(peer.id));
} }
@@ -992,9 +1018,7 @@ class DiscoveredPeerCard extends BasePeerCard {
} }
if (gFFI.userModel.userName.isNotEmpty) { if (gFFI.userModel.userName.isNotEmpty) {
if (!gFFI.abModel.idContainBy(peer.id)) { menuItems.add(_addToAb(peer));
menuItems.add(_addToAb(peer));
}
} }
menuItems.add(MenuEntryDivider()); menuItems.add(MenuEntryDivider());
@@ -1019,37 +1043,55 @@ class AddressBookPeerCard extends BasePeerCard {
Future<List<MenuEntryBase<String>>> _buildMenuItems( Future<List<MenuEntryBase<String>>> _buildMenuItems(
BuildContext context) async { BuildContext context) async {
final List<MenuEntryBase<String>> menuItems = [ final List<MenuEntryBase<String>> menuItems = [
_connectAction(context, peer), _connectAction(context),
_transferFileAction(context, peer.id), _transferFileAction(context),
]; ];
if (isDesktop && peer.platform != kPeerPlatformAndroid) { if (isDesktop && peer.platform != kPeerPlatformAndroid) {
menuItems.add(_tcpTunnelingAction(context, peer.id)); menuItems.add(_tcpTunnelingAction(context));
} }
// menuItems.add(await _openNewConnInOptAction(peer.id)); // menuItems.add(await _openNewConnInOptAction(peer.id));
menuItems.add(await _forceAlwaysRelayAction(peer.id)); // menuItems.add(await _forceAlwaysRelayAction(peer.id));
if (Platform.isWindows && peer.platform == kPeerPlatformWindows) { if (isWindows && peer.platform == kPeerPlatformWindows) {
menuItems.add(_rdpAction(context, peer.id)); menuItems.add(_rdpAction(context, peer.id));
} }
if (Platform.isWindows) { if (isWindows) {
menuItems.add(_createShortCutAction(peer.id)); menuItems.add(_createShortCutAction(peer.id));
} }
menuItems.add(MenuEntryDivider()); if (gFFI.abModel.current.canWrite()) {
menuItems.add(_renameAction(peer.id)); menuItems.add(MenuEntryDivider());
if (peer.hash.isNotEmpty) { if (isMobile || isDesktop || isWebDesktop) {
menuItems.add(_unrememberPasswordAction(peer.id)); menuItems.add(_renameAction(peer.id));
}
if (gFFI.abModel.current.isPersonal() && peer.hash.isNotEmpty) {
menuItems.add(_unrememberPasswordAction(peer.id));
}
if (!gFFI.abModel.current.isPersonal()) {
menuItems.add(_changeSharedAbPassword());
}
if (gFFI.abModel.currentAbTags.isNotEmpty) {
menuItems.add(_editTagAction(peer.id));
}
} }
if (gFFI.abModel.tags.isNotEmpty) { final addressbooks = gFFI.abModel.addressBooksCanWrite();
menuItems.add(_editTagAction(peer.id)); if (gFFI.peerTabModel.currentTab == PeerTabIndex.ab.index) {
addressbooks.remove(gFFI.abModel.currentName.value);
}
if (addressbooks.isNotEmpty) {
menuItems.add(_addToAb(peer));
}
menuItems.add(_existIn());
if (gFFI.abModel.current.canWrite()) {
menuItems.add(MenuEntryDivider());
menuItems.add(_removeAction(peer.id));
} }
menuItems.add(MenuEntryDivider());
menuItems.add(_removeAction(peer.id));
return menuItems; return menuItems;
} }
// address book does not need to update
@protected @protected
@override @override
void _update() => gFFI.abModel.pullAb(quiet: true); void _update() =>
{}; //gFFI.abModel.pullAb(force: ForcePullAb.current, quiet: true);
@protected @protected
MenuEntryBase<String> _editTagAction(String id) { MenuEntryBase<String> _editTagAction(String id) {
@@ -1060,8 +1102,7 @@ class AddressBookPeerCard extends BasePeerCard {
), ),
proc: () { proc: () {
editAbTagDialog(gFFI.abModel.getPeerTags(id), (selectedTag) async { editAbTagDialog(gFFI.abModel.getPeerTags(id), (selectedTag) async {
gFFI.abModel.changeTagForPeer(id, selectedTag); await gFFI.abModel.changeTagForPeers([id], selectedTag);
gFFI.abModel.pushAb();
}); });
}, },
padding: super.menuPadding, padding: super.menuPadding,
@@ -1073,6 +1114,53 @@ class AddressBookPeerCard extends BasePeerCard {
@override @override
Future<String> _getAlias(String id) async => Future<String> _getAlias(String id) async =>
gFFI.abModel.find(id)?.alias ?? ''; gFFI.abModel.find(id)?.alias ?? '';
MenuEntryBase<String> _changeSharedAbPassword() {
return MenuEntryButton<String>(
childBuilder: (TextStyle? style) => Text(
translate(
peer.password.isEmpty ? 'Set shared password' : 'Change Password'),
style: style,
),
proc: () {
setSharedAbPasswordDialog(gFFI.abModel.currentName.value, peer);
},
padding: super.menuPadding,
dismissOnClicked: true,
);
}
MenuEntryBase<String> _existIn() {
final names = gFFI.abModel.idExistIn(peer.id);
final text = names.join(', ');
return MenuEntryButton<String>(
childBuilder: (TextStyle? style) => Text(
translate('Exist in'),
style: style,
),
proc: () {
gFFI.dialogManager.show((setState, close, context) {
return CustomAlertDialog(
title: Text(translate('Exist in')),
content: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [Text(text)]),
actions: [
dialogButton(
"OK",
icon: Icon(Icons.done_rounded),
onPressed: close,
),
],
onSubmit: close,
onCancel: close,
);
});
},
padding: super.menuPadding,
dismissOnClicked: true,
);
}
} }
class MyGroupPeerCard extends BasePeerCard { class MyGroupPeerCard extends BasePeerCard {
@@ -1087,18 +1175,18 @@ class MyGroupPeerCard extends BasePeerCard {
Future<List<MenuEntryBase<String>>> _buildMenuItems( Future<List<MenuEntryBase<String>>> _buildMenuItems(
BuildContext context) async { BuildContext context) async {
final List<MenuEntryBase<String>> menuItems = [ final List<MenuEntryBase<String>> menuItems = [
_connectAction(context, peer), _connectAction(context),
_transferFileAction(context, peer.id), _transferFileAction(context),
]; ];
if (isDesktop && peer.platform != kPeerPlatformAndroid) { if (isDesktop && peer.platform != kPeerPlatformAndroid) {
menuItems.add(_tcpTunnelingAction(context, peer.id)); menuItems.add(_tcpTunnelingAction(context));
} }
// menuItems.add(await _openNewConnInOptAction(peer.id)); // menuItems.add(await _openNewConnInOptAction(peer.id));
// menuItems.add(await _forceAlwaysRelayAction(peer.id)); // menuItems.add(await _forceAlwaysRelayAction(peer.id));
if (Platform.isWindows && peer.platform == kPeerPlatformWindows) { if (isWindows && peer.platform == kPeerPlatformWindows) {
menuItems.add(_rdpAction(context, peer.id)); menuItems.add(_rdpAction(context, peer.id));
} }
if (Platform.isWindows) { if (isWindows) {
menuItems.add(_createShortCutAction(peer.id)); menuItems.add(_createShortCutAction(peer.id));
} }
// menuItems.add(MenuEntryDivider()); // menuItems.add(MenuEntryDivider());
@@ -1107,9 +1195,7 @@ class MyGroupPeerCard extends BasePeerCard {
// menuItems.add(_unrememberPasswordAction(peer.id)); // menuItems.add(_unrememberPasswordAction(peer.id));
// } // }
if (gFFI.userModel.userName.isNotEmpty) { if (gFFI.userModel.userName.isNotEmpty) {
if (!gFFI.abModel.idContainBy(peer.id)) { menuItems.add(_addToAb(peer));
menuItems.add(_addToAb(peer));
}
} }
return menuItems; return menuItems;
} }
@@ -1120,6 +1206,7 @@ class MyGroupPeerCard extends BasePeerCard {
} }
void _rdpDialog(String id) async { void _rdpDialog(String id) async {
final maxLength = bind.mainMaxEncryptLen();
final port = await bind.mainGetPeerOption(id: id, key: 'rdp_port'); final port = await bind.mainGetPeerOption(id: id, key: 'rdp_port');
final username = await bind.mainGetPeerOption(id: id, key: 'rdp_username'); final username = await bind.mainGetPeerOption(id: id, key: 'rdp_username');
final portController = TextEditingController(text: port); final portController = TextEditingController(text: port);
@@ -1174,50 +1261,54 @@ void _rdpDialog(String id) async {
), ),
], ],
).marginOnly(bottom: isDesktop ? 8 : 0), ).marginOnly(bottom: isDesktop ? 8 : 0),
Row( Obx(() => Row(
children: [ children: [
isDesktop stateGlobal.isPortrait.isFalse
? ConstrainedBox( ? ConstrainedBox(
constraints: const BoxConstraints(minWidth: 140), constraints: const BoxConstraints(minWidth: 140),
child: Text( child: Text(
"${translate('Username')}:", "${translate('Username')}:",
textAlign: TextAlign.right, textAlign: TextAlign.right,
).marginOnly(right: 10)) ).marginOnly(right: 10))
: SizedBox.shrink(), : SizedBox.shrink(),
Expanded( Expanded(
child: TextField( child: TextField(
decoration: InputDecoration(
labelText: isDesktop ? null : translate('Username')),
controller: userController,
),
),
],
).marginOnly(bottom: isDesktop ? 8 : 0),
Row(
children: [
isDesktop
? ConstrainedBox(
constraints: const BoxConstraints(minWidth: 140),
child: Text(
"${translate('Password')}:",
textAlign: TextAlign.right,
).marginOnly(right: 10))
: SizedBox.shrink(),
Expanded(
child: Obx(() => TextField(
obscureText: secure.value,
decoration: InputDecoration( decoration: InputDecoration(
labelText: isDesktop ? null : translate('Password'), labelText:
suffixIcon: IconButton( isDesktop ? null : translate('Username')),
onPressed: () => secure.value = !secure.value, controller: userController,
icon: Icon(secure.value ),
? Icons.visibility_off ),
: Icons.visibility))), ],
controller: passwordController, ).marginOnly(bottom: stateGlobal.isPortrait.isFalse ? 8 : 0)),
)), Obx(() => Row(
), children: [
], stateGlobal.isPortrait.isFalse
) ? ConstrainedBox(
constraints: const BoxConstraints(minWidth: 140),
child: Text(
"${translate('Password')}:",
textAlign: TextAlign.right,
).marginOnly(right: 10))
: SizedBox.shrink(),
Expanded(
child: Obx(() => TextField(
obscureText: secure.value,
maxLength: maxLength,
decoration: InputDecoration(
labelText:
isDesktop ? null : translate('Password'),
suffixIcon: IconButton(
onPressed: () =>
secure.value = !secure.value,
icon: Icon(secure.value
? Icons.visibility_off
: Icons.visibility))),
controller: passwordController,
)),
),
],
))
], ],
), ),
), ),
@@ -1305,24 +1396,32 @@ class TagPainter extends CustomPainter {
} }
} }
void connectInPeerTab(BuildContext context, String id, PeerTabIndex tab, void connectInPeerTab(BuildContext context, Peer peer, PeerTabIndex tab,
{bool isFileTransfer = false, {bool isFileTransfer = false,
bool isTcpTunneling = false, bool isTcpTunneling = false,
bool isRDP = false}) async { bool isRDP = false}) async {
var password = '';
bool isSharedPassword = false;
if (tab == PeerTabIndex.ab) { if (tab == PeerTabIndex.ab) {
// If recent peer's alias is empty, set it to ab's alias // If recent peer's alias is empty, set it to ab's alias
// Because the platform is not set, it may not take effect, but it is more important not to display if the connection is not successful // Because the platform is not set, it may not take effect, but it is more important not to display if the connection is not successful
Peer? p = gFFI.abModel.find(id); if (peer.alias.isNotEmpty &&
if (p != null && (await bind.mainGetPeerOption(id: peer.id, key: "alias")).isEmpty) {
p.alias.isNotEmpty &&
(await bind.mainGetPeerOption(id: id, key: "alias")).isEmpty) {
await bind.mainSetPeerAlias( await bind.mainSetPeerAlias(
id: id, id: peer.id,
alias: p.alias, alias: peer.alias,
); );
} }
if (!gFFI.abModel.current.isPersonal()) {
if (peer.password.isNotEmpty) {
password = peer.password;
isSharedPassword = true;
}
}
} }
connect(context, id, connect(context, peer.id,
password: password,
isSharedPassword: isSharedPassword,
isFileTransfer: isFileTransfer, isFileTransfer: isFileTransfer,
isTcpTunneling: isTcpTunneling, isTcpTunneling: isTcpTunneling,
isRDP: isRDP); isRDP: isRDP);

View File

@@ -13,8 +13,10 @@ import 'package:flutter_hbb/desktop/widgets/material_mod_popup_menu.dart'
as mod_menu; as mod_menu;
import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart'; import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
import 'package:flutter_hbb/models/ab_model.dart'; import 'package:flutter_hbb/models/ab_model.dart';
import 'package:flutter_hbb/models/peer_model.dart';
import 'package:flutter_hbb/models/peer_tab_model.dart'; import 'package:flutter_hbb/models/peer_tab_model.dart';
import 'package:flutter_hbb/models/state_model.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';
@@ -36,7 +38,7 @@ class _TabEntry {
} }
EdgeInsets? _menuPadding() { EdgeInsets? _menuPadding() {
return isDesktop ? kDesktopMenuPadding : null; return (isDesktop || isWebDesktop) ? kDesktopMenuPadding : null;
} }
class _PeerTabPageState extends State<PeerTabPage> class _PeerTabPageState extends State<PeerTabPage>
@@ -61,7 +63,9 @@ class _PeerTabPageState extends State<PeerTabPage>
AddressBook( AddressBook(
menuPadding: _menuPadding(), menuPadding: _menuPadding(),
), ),
({dynamic hint}) => gFFI.abModel.pullAb(force: hint == null)), ({dynamic hint}) => gFFI.abModel.pullAb(
force: hint == null ? ForcePullAb.listAndCurrent : null,
quiet: false)),
_TabEntry( _TabEntry(
MyGroup( MyGroup(
menuPadding: _menuPadding(), menuPadding: _menuPadding(),
@@ -71,9 +75,14 @@ class _PeerTabPageState extends State<PeerTabPage>
]; ];
RelativeRect? mobileTabContextMenuPos; RelativeRect? mobileTabContextMenuPos;
@override final isOptVisiableFixed = isOptionFixed(kOptionPeerTabVisible);
void initState() {
final uiType = bind.getLocalFlutterOption(k: 'peer-card-ui-type'); _PeerTabPageState() {
_loadLocalOptions();
}
void _loadLocalOptions() {
final uiType = bind.getLocalFlutterOption(k: kOptionPeerCardUiType);
if (uiType != '') { if (uiType != '') {
peerCardUiType.value = int.parse(uiType) == 0 peerCardUiType.value = int.parse(uiType) == 0
? PeerUiType.grid ? PeerUiType.grid
@@ -82,8 +91,7 @@ class _PeerTabPageState extends State<PeerTabPage>
: PeerUiType.list; : PeerUiType.list;
} }
hideAbTagsPanel.value = hideAbTagsPanel.value =
bind.mainGetLocalOption(key: "hideAbTagsPanel").isNotEmpty; bind.mainGetLocalOption(key: kOptionHideAbTagsPanel) == 'Y';
super.initState();
} }
Future<void> handleTabSelection(int tabIndex) async { Future<void> handleTabSelection(int tabIndex) async {
@@ -100,31 +108,33 @@ class _PeerTabPageState extends State<PeerTabPage>
Widget build(BuildContext context) { Widget build(BuildContext context) {
final model = Provider.of<PeerTabModel>(context); final model = Provider.of<PeerTabModel>(context);
Widget selectionWrap(Widget widget) { Widget selectionWrap(Widget widget) {
return model.multiSelectionMode ? createMultiSelectionBar() : widget; return model.multiSelectionMode ? createMultiSelectionBar(model) : widget;
} }
return Column( return Column(
textBaseline: TextBaseline.ideographic, textBaseline: TextBaseline.ideographic,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
SizedBox( Obx(() => SizedBox(
height: 32, height: 32,
child: Container( child: Container(
padding: isDesktop ? null : EdgeInsets.symmetric(horizontal: 2), padding: stateGlobal.isPortrait.isTrue
child: selectionWrap(Row( ? EdgeInsets.symmetric(horizontal: 2)
crossAxisAlignment: CrossAxisAlignment.center, : null,
children: [ child: selectionWrap(Row(
Expanded( crossAxisAlignment: CrossAxisAlignment.center,
child: children: [
visibleContextMenuListener(_createSwitchBar(context))), Expanded(
if (isMobile) child: visibleContextMenuListener(
..._mobileRightActions(context) _createSwitchBar(context))),
else if (stateGlobal.isPortrait.isTrue)
..._desktopRightActions(context) ..._portraitRightActions(context)
], else
)), ..._landscapeRightActions(context)
), ],
).paddingOnly(right: isDesktop ? 12 : 0), )),
),
).paddingOnly(right: stateGlobal.isPortrait.isTrue ? 0 : 12)),
_createPeersView(), _createPeersView(),
], ],
); );
@@ -132,11 +142,13 @@ class _PeerTabPageState extends State<PeerTabPage>
Widget _createSwitchBar(BuildContext context) { Widget _createSwitchBar(BuildContext context) {
final model = Provider.of<PeerTabModel>(context); final model = Provider.of<PeerTabModel>(context);
var counter = -1;
return ListView( return ReorderableListView(
buildDefaultDragHandles: false,
onReorder: model.reorder,
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
physics: NeverScrollableScrollPhysics(), physics: NeverScrollableScrollPhysics(),
children: model.visibleIndexs.map((t) { children: model.visibleEnabledOrderedIndexs.map((t) {
final selected = model.currentTab == t; final selected = model.currentTab == t;
final color = selected final color = selected
? MyTheme.tabbar(context).selectedTextColor ? MyTheme.tabbar(context).selectedTextColor
@@ -150,48 +162,56 @@ class _PeerTabPageState extends State<PeerTabPage>
border: Border( border: Border(
bottom: BorderSide(width: 2, color: color!), bottom: BorderSide(width: 2, color: color!),
)); ));
return Obx(() => InkWell( counter += 1;
child: Container( return ReorderableDragStartListener(
decoration: (hover.value key: ValueKey(t),
? (selected ? decoBorder : deco) index: counter,
: (selected ? decoBorder : null)), child: Obx(() => Tooltip(
child: Tooltip(
preferBelow: false, preferBelow: false,
message: model.tabTooltip(t), message: model.tabTooltip(t),
onTriggered: isMobile ? mobileShowTabVisibilityMenu : null, onTriggered: isMobile ? mobileShowTabVisibilityMenu : null,
child: Icon(model.tabIcon(t), color: color), child: InkWell(
).paddingSymmetric(horizontal: 4), child: Container(
).paddingSymmetric(horizontal: 4), decoration: (hover.value
onTap: () async { ? (selected ? decoBorder : deco)
await handleTabSelection(t); : (selected ? decoBorder : null)),
await bind.setLocalFlutterOption( child: Icon(model.tabIcon(t), color: color)
k: 'peer-tab-index', v: t.toString()); .paddingSymmetric(horizontal: 4),
}, ).paddingSymmetric(horizontal: 4),
onHover: (value) => hover.value = value, onTap: isOptionFixed(kOptionPeerTabIndex)
)); ? null
: () async {
await handleTabSelection(t);
await bind.setLocalFlutterOption(
k: kOptionPeerTabIndex, v: t.toString());
},
onHover: (value) => hover.value = value,
),
)));
}).toList()); }).toList());
} }
Widget _createPeersView() { Widget _createPeersView() {
final model = Provider.of<PeerTabModel>(context); final model = Provider.of<PeerTabModel>(context);
Widget child; Widget child;
if (model.visibleIndexs.isEmpty) { if (model.visibleEnabledOrderedIndexs.isEmpty) {
child = visibleContextMenuListener(Row( child = visibleContextMenuListener(Row(
children: [Expanded(child: InkWell())], children: [Expanded(child: InkWell())],
)); ));
} else { } else {
if (model.visibleIndexs.contains(model.currentTab)) { if (model.visibleEnabledOrderedIndexs.contains(model.currentTab)) {
child = entries[model.currentTab].widget; child = entries[model.currentTab].widget;
} else { } else {
debugPrint("should not happen! currentTab not in visibleIndexs"); debugPrint("should not happen! currentTab not in visibleIndexs");
Future.delayed(Duration.zero, () { Future.delayed(Duration.zero, () {
model.setCurrentTab(model.indexs[0]); model.setCurrentTab(model.visibleEnabledOrderedIndexs[0]);
}); });
child = entries[0].widget; child = entries[0].widget;
} }
} }
return Expanded( return Expanded(
child: child.marginSymmetric(vertical: isDesktop ? 12.0 : 6.0)); child: child.marginSymmetric(
vertical: (isDesktop || isWebDesktop) ? 12.0 : 6.0));
} }
Widget _createRefresh( Widget _createRefresh(
@@ -200,22 +220,23 @@ class _PeerTabPageState extends State<PeerTabPage>
final textColor = Theme.of(context).textTheme.titleLarge?.color; final textColor = Theme.of(context).textTheme.titleLarge?.color;
return Offstage( return Offstage(
offstage: model.currentTab != index.index, offstage: model.currentTab != index.index,
child: RefreshWidget( child: Tooltip(
onPressed: () { message: translate('Refresh'),
if (gFFI.peerTabModel.currentTab < entries.length) { child: RefreshWidget(
entries[gFFI.peerTabModel.currentTab].load(); onPressed: () {
} if (gFFI.peerTabModel.currentTab < entries.length) {
}, entries[gFFI.peerTabModel.currentTab].load();
spinning: loading, }
child: RotatedBox( },
quarterTurns: 2, spinning: loading,
child: Tooltip( child: RotatedBox(
message: translate('Refresh'), quarterTurns: 2,
child: Icon( child: Icon(
Icons.refresh, Icons.refresh,
size: 18, size: 18,
color: textColor, color: textColor,
)))), ))),
),
); );
} }
@@ -227,6 +248,7 @@ class _PeerTabPageState extends State<PeerTabPage>
final textColor = Theme.of(context).textTheme.titleLarge?.color; final textColor = Theme.of(context).textTheme.titleLarge?.color;
final model = Provider.of<PeerTabModel>(context); final model = Provider.of<PeerTabModel>(context);
return _hoverAction( return _hoverAction(
toolTip: translate('Select'),
context: context, context: context,
onTap: () { onTap: () {
model.setMultiSelectionMode(true); model.setMultiSelectionMode(true);
@@ -234,34 +256,38 @@ class _PeerTabPageState extends State<PeerTabPage>
Navigator.pop(context); Navigator.pop(context);
} }
}, },
child: Tooltip( child: SvgPicture.asset(
message: translate('Select'), "assets/checkbox-outline.svg",
child: SvgPicture.asset( width: 18,
"assets/checkbox-outline.svg", height: 18,
width: 18, colorFilter: svgColor(textColor),
height: 18, ),
colorFilter: svgColor(textColor),
)),
); );
} }
void mobileShowTabVisibilityMenu() { void mobileShowTabVisibilityMenu() {
final model = gFFI.peerTabModel; final model = gFFI.peerTabModel;
final items = List<PopupMenuItem>.empty(growable: true); final items = List<PopupMenuItem>.empty(growable: true);
for (int i = 0; i < model.tabNames.length; i++) { for (int i = 0; i < PeerTabModel.maxTabCount; i++) {
if (!model.isEnabled[i]) continue;
items.add(PopupMenuItem( items.add(PopupMenuItem(
height: kMinInteractiveDimension * 0.8, height: kMinInteractiveDimension * 0.8,
onTap: () => model.setTabVisible(i, !model.isVisible[i]), onTap: isOptVisiableFixed
? null
: () => model.setTabVisible(i, !model.isVisibleEnabled[i]),
enabled: !isOptVisiableFixed,
child: Row( child: Row(
children: [ children: [
Checkbox( Checkbox(
value: model.isVisible[i], value: model.isVisibleEnabled[i],
onChanged: (_) { onChanged: isOptVisiableFixed
model.setTabVisible(i, !model.isVisible[i]); ? null
if (Navigator.canPop(context)) { : (_) {
Navigator.pop(context); model.setTabVisible(i, !model.isVisibleEnabled[i]);
} if (Navigator.canPop(context)) {
}), Navigator.pop(context);
}
}),
Expanded(child: Text(model.tabTooltip(i))), Expanded(child: Text(model.tabTooltip(i))),
], ],
), ),
@@ -274,7 +300,7 @@ class _PeerTabPageState extends State<PeerTabPage>
} }
Widget visibleContextMenuListener(Widget child) { Widget visibleContextMenuListener(Widget child) {
if (isMobile) { if (!(isDesktop || isWebDesktop)) {
return GestureDetector( return GestureDetector(
onLongPressDown: (e) { onLongPressDown: (e) {
final x = e.globalPosition.dx; final x = e.globalPosition.dx;
@@ -307,18 +333,21 @@ class _PeerTabPageState extends State<PeerTabPage>
Widget visibleContextMenu(CancelFunc cancelFunc) { Widget visibleContextMenu(CancelFunc cancelFunc) {
final model = Provider.of<PeerTabModel>(context); final model = Provider.of<PeerTabModel>(context);
final menu = List<MenuEntrySwitch>.empty(growable: true); final menu = List<MenuEntrySwitchSync>.empty(growable: true);
for (int i = 0; i < model.tabNames.length; i++) { for (int i = 0; i < model.orders.length; i++) {
menu.add(MenuEntrySwitch( int tabIndex = model.orders[i];
if (tabIndex < 0 || tabIndex >= PeerTabModel.maxTabCount) continue;
if (!model.isEnabled[tabIndex]) continue;
menu.add(MenuEntrySwitchSync(
switchType: SwitchType.scheckbox, switchType: SwitchType.scheckbox,
text: model.tabTooltip(i), text: model.tabTooltip(tabIndex),
getter: () async { currentValue: model.isVisibleEnabled[tabIndex],
return model.isVisible[i];
},
setter: (show) async { setter: (show) async {
model.setTabVisible(i, show); model.setTabVisible(tabIndex, show);
cancelFunc(); // Do not hide the current menu (checkbox)
})); // cancelFunc();
},
enabled: (!isOptVisiableFixed).obs));
} }
return mod_menu.PopupMenu( return mod_menu.PopupMenu(
items: menu items: menu
@@ -333,8 +362,7 @@ class _PeerTabPageState extends State<PeerTabPage>
.toList()); .toList());
} }
Widget createMultiSelectionBar() { Widget createMultiSelectionBar(PeerTabModel model) {
final model = Provider.of<PeerTabModel>(context);
return Row( return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
@@ -352,7 +380,7 @@ class _PeerTabPageState extends State<PeerTabPage>
Row( Row(
children: [ children: [
selectionCount(model.selectedPeers.length), selectionCount(model.selectedPeers.length),
selectAll(), selectAll(model),
closeSelection(), closeSelection(),
], ],
) )
@@ -367,14 +395,15 @@ class _PeerTabPageState extends State<PeerTabPage>
} }
return _hoverAction( return _hoverAction(
context: context, context: context,
toolTip: translate('Delete'),
onTap: () { onTap: () {
onSubmit() async { onSubmit() async {
final peers = model.selectedPeers; final peers = model.selectedPeers;
switch (model.currentTab) { switch (model.currentTab) {
case 0: case 0:
peers.map((p) async { for (var p in peers) {
await bind.mainRemovePeer(id: p.id); await bind.mainRemovePeer(id: p.id);
}).toList(); }
await bind.mainLoadRecentPeers(); await bind.mainLoadRecentPeers();
break; break;
case 1: case 1:
@@ -386,27 +415,13 @@ class _PeerTabPageState extends State<PeerTabPage>
await bind.mainLoadFavPeers(); await bind.mainLoadFavPeers();
break; break;
case 2: case 2:
peers.map((p) async { for (var p in peers) {
await bind.mainRemoveDiscovered(id: p.id); await bind.mainRemoveDiscovered(id: p.id);
}).toList(); }
await bind.mainLoadLanPeers(); await bind.mainLoadLanPeers();
break; break;
case 3: case 3:
{ await gFFI.abModel.deletePeers(peers.map((p) => p.id).toList());
bool hasSynced = false;
if (shouldSyncAb()) {
for (var p in peers) {
if (await bind.mainPeerExists(id: p.id)) {
hasSynced = true;
}
}
}
gFFI.abModel.deletePeers(peers.map((p) => p.id).toList());
final future = gFFI.abModel.pushAb();
if (hasSynced) {
gFFI.abModel.reSyncToast(future);
}
}
break; break;
default: default:
break; break;
@@ -415,11 +430,9 @@ class _PeerTabPageState extends State<PeerTabPage>
if (model.currentTab != 3) showToast(translate('Successful')); if (model.currentTab != 3) showToast(translate('Successful'));
} }
deletePeerConfirmDialog(onSubmit, translate('Delete')); deleteConfirmDialog(onSubmit, translate('Delete'));
}, },
child: Tooltip( child: Icon(Icons.delete, color: Colors.red));
message: translate('Delete'),
child: Icon(Icons.delete, color: Colors.red)));
} }
Widget addSelectionToFav() { Widget addSelectionToFav() {
@@ -429,6 +442,7 @@ class _PeerTabPageState extends State<PeerTabPage>
model.currentTab != PeerTabIndex.recent.index, // show based on recent model.currentTab != PeerTabIndex.recent.index, // show based on recent
child: _hoverAction( child: _hoverAction(
context: context, context: context,
toolTip: translate('Add to Favorites'),
onTap: () async { onTap: () async {
final peers = model.selectedPeers; final peers = model.selectedPeers;
final favs = (await bind.mainGetFav()).toList(); final favs = (await bind.mainGetFav()).toList();
@@ -441,38 +455,29 @@ class _PeerTabPageState extends State<PeerTabPage>
model.setMultiSelectionMode(false); model.setMultiSelectionMode(false);
showToast(translate('Successful')); showToast(translate('Successful'));
}, },
child: Tooltip( child: Icon(PeerTabModel.icons[PeerTabIndex.fav.index]),
message: translate('Add to Favorites'), ).marginOnly(left: !(isDesktop || isWebDesktop) ? 11 : 6),
child: Icon(model.icons[PeerTabIndex.fav.index])),
).marginOnly(left: isMobile ? 11 : 6),
); );
} }
Widget addSelectionToAb() { Widget addSelectionToAb() {
final model = Provider.of<PeerTabModel>(context); final model = Provider.of<PeerTabModel>(context);
final addressbooks = gFFI.abModel.addressBooksCanWrite();
if (model.currentTab == PeerTabIndex.ab.index) {
addressbooks.remove(gFFI.abModel.currentName.value);
}
return Offstage( return Offstage(
offstage: offstage: !gFFI.userModel.isLogin || addressbooks.isEmpty,
!gFFI.userModel.isLogin || model.currentTab == PeerTabIndex.ab.index,
child: _hoverAction( child: _hoverAction(
context: context, context: context,
toolTip: translate('Add to address book'),
onTap: () { onTap: () {
if (gFFI.abModel.isFull(true)) { final peers = model.selectedPeers.map((e) => Peer.copy(e)).toList();
return; addPeersToAbDialog(peers);
}
final peers = model.selectedPeers;
gFFI.abModel.addPeers(peers);
final future = gFFI.abModel.pushAb();
model.setMultiSelectionMode(false); model.setMultiSelectionMode(false);
Future.delayed(Duration.zero, () async {
await future;
await Future.delayed(Duration(seconds: 2)); // toast
gFFI.abModel.isFull(true);
});
}, },
child: Tooltip( child: Icon(PeerTabModel.icons[PeerTabIndex.ab.index]),
message: translate('Add to address book'), ).marginOnly(left: !(isDesktop || isWebDesktop) ? 11 : 6),
child: Icon(model.icons[PeerTabIndex.ab.index])),
).marginOnly(left: isMobile ? 11 : 6),
); );
} }
@@ -481,22 +486,21 @@ class _PeerTabPageState extends State<PeerTabPage>
return Offstage( return Offstage(
offstage: !gFFI.userModel.isLogin || offstage: !gFFI.userModel.isLogin ||
model.currentTab != PeerTabIndex.ab.index || model.currentTab != PeerTabIndex.ab.index ||
gFFI.abModel.tags.isEmpty, gFFI.abModel.currentAbTags.isEmpty,
child: _hoverAction( child: _hoverAction(
context: context, context: context,
toolTip: translate('Edit Tag'),
onTap: () { onTap: () {
editAbTagDialog(List.empty(), (selectedTags) async { editAbTagDialog(List.empty(), (selectedTags) async {
final peers = model.selectedPeers; final peers = model.selectedPeers;
gFFI.abModel.changeTagForPeers( await gFFI.abModel.changeTagForPeers(
peers.map((p) => p.id).toList(), selectedTags); peers.map((p) => p.id).toList(), selectedTags);
gFFI.abModel.pushAb();
model.setMultiSelectionMode(false); model.setMultiSelectionMode(false);
showToast(translate('Successful')); showToast(translate('Successful'));
}); });
}, },
child: Tooltip( child: Icon(Icons.tag))
message: translate('Edit Tag'), child: Icon(Icons.tag))) .marginOnly(left: !(isDesktop || isWebDesktop) ? 11 : 6),
.marginOnly(left: isMobile ? 11 : 6),
); );
} }
@@ -507,18 +511,17 @@ class _PeerTabPageState extends State<PeerTabPage>
); );
} }
Widget selectAll() { Widget selectAll(PeerTabModel model) {
final model = Provider.of<PeerTabModel>(context);
return Offstage( return Offstage(
offstage: offstage:
model.selectedPeers.length >= model.currentTabCachedPeers.length, model.selectedPeers.length >= model.currentTabCachedPeers.length,
child: _hoverAction( child: _hoverAction(
context: context, context: context,
toolTip: translate('Select All'),
onTap: () { onTap: () {
model.selectAll(); model.selectAll();
}, },
child: Tooltip( child: Icon(Icons.select_all),
message: translate('Select All'), child: Icon(Icons.select_all)),
).marginOnly(left: 6), ).marginOnly(left: 6),
); );
} }
@@ -527,36 +530,37 @@ class _PeerTabPageState extends State<PeerTabPage>
final model = Provider.of<PeerTabModel>(context); final model = Provider.of<PeerTabModel>(context);
return _hoverAction( return _hoverAction(
context: context, context: context,
toolTip: translate('Close'),
onTap: () { onTap: () {
model.setMultiSelectionMode(false); model.setMultiSelectionMode(false);
}, },
child: child: Icon(Icons.clear))
Tooltip(message: translate('Close'), child: Icon(Icons.clear)))
.marginOnly(left: 6); .marginOnly(left: 6);
} }
Widget _toggleTags() { Widget _toggleTags() {
return _hoverAction( return _hoverAction(
context: context, context: context,
toolTip: translate('Toggle Tags'),
hoverableWhenfalse: hideAbTagsPanel, hoverableWhenfalse: hideAbTagsPanel,
child: Tooltip( child: Icon(
message: translate('Toggle Tags'), Icons.tag_rounded,
child: Icon( size: 18,
Icons.tag_rounded, ),
size: 18,
)),
onTap: () async { onTap: () async {
await bind.mainSetLocalOption( await bind.mainSetLocalOption(
key: "hideAbTagsPanel", value: hideAbTagsPanel.value ? "" : "Y"); key: kOptionHideAbTagsPanel,
value: hideAbTagsPanel.value ? defaultOptionNo : "Y");
hideAbTagsPanel.value = !hideAbTagsPanel.value; hideAbTagsPanel.value = !hideAbTagsPanel.value;
}); });
} }
List<Widget> _desktopRightActions(BuildContext context) { List<Widget> _landscapeRightActions(BuildContext context) {
final model = Provider.of<PeerTabModel>(context); final model = Provider.of<PeerTabModel>(context);
return [ return [
const PeerSearchBar().marginOnly(right: isMobile ? 0 : 13), const PeerSearchBar().marginOnly(right: 13),
_createRefresh(index: PeerTabIndex.ab, loading: gFFI.abModel.abLoading), _createRefresh(
index: PeerTabIndex.ab, loading: gFFI.abModel.currentAbLoading),
_createRefresh( _createRefresh(
index: PeerTabIndex.group, loading: gFFI.groupModel.groupLoading), index: PeerTabIndex.group, loading: gFFI.groupModel.groupLoading),
Offstage( Offstage(
@@ -575,12 +579,12 @@ class _PeerTabPageState extends State<PeerTabPage>
]; ];
} }
List<Widget> _mobileRightActions(BuildContext context) { List<Widget> _portraitRightActions(BuildContext context) {
final model = Provider.of<PeerTabModel>(context); final model = Provider.of<PeerTabModel>(context);
final screenWidth = MediaQuery.of(context).size.width; final screenWidth = MediaQuery.of(context).size.width;
final leftIconSize = Theme.of(context).iconTheme.size ?? 24; final leftIconSize = Theme.of(context).iconTheme.size ?? 24;
final leftActionsSize = final leftActionsSize =
(leftIconSize + (4 + 4) * 2) * model.visibleIndexs.length; (leftIconSize + (4 + 4) * 2) * model.visibleEnabledOrderedIndexs.length;
final availableWidth = screenWidth - 10 * 2 - leftActionsSize - 2 * 2; final availableWidth = screenWidth - 10 * 2 - leftActionsSize - 2 * 2;
final searchWidth = 120; final searchWidth = 120;
final otherActionWidth = 18 + 10; final otherActionWidth = 18 + 10;
@@ -593,14 +597,13 @@ class _PeerTabPageState extends State<PeerTabPage>
(BuildContext context, Future<void> Function() showMenu) { (BuildContext context, Future<void> Function() showMenu) {
return _hoverAction( return _hoverAction(
context: context, context: context,
child: Tooltip( toolTip: translate('More'),
message: translate('More'), child: SvgPicture.asset(
child: SvgPicture.asset( "assets/chevron_up_chevron_down.svg",
"assets/chevron_up_chevron_down.svg", width: 18,
width: 18, height: 18,
height: 18, colorFilter: svgColor(textColor),
colorFilter: svgColor(textColor), ),
)),
onTap: showMenu, onTap: showMenu,
); );
}, },
@@ -624,7 +627,8 @@ class _PeerTabPageState extends State<PeerTabPage>
List<Widget> actions = [ List<Widget> actions = [
const PeerSearchBar(), const PeerSearchBar(),
if (model.currentTab == PeerTabIndex.ab.index) if (model.currentTab == PeerTabIndex.ab.index)
_createRefresh(index: PeerTabIndex.ab, loading: gFFI.abModel.abLoading), _createRefresh(
index: PeerTabIndex.ab, loading: gFFI.abModel.currentAbLoading),
if (model.currentTab == PeerTabIndex.group.index) if (model.currentTab == PeerTabIndex.group.index)
_createRefresh( _createRefresh(
index: PeerTabIndex.group, loading: gFFI.groupModel.groupLoading), index: PeerTabIndex.group, loading: gFFI.groupModel.groupLoading),
@@ -674,18 +678,17 @@ class _PeerSearchBarState extends State<PeerSearchBar> {
? _buildSearchBar() ? _buildSearchBar()
: _hoverAction( : _hoverAction(
context: context, context: context,
toolTip: translate('Search'),
padding: const EdgeInsets.only(right: 2), padding: const EdgeInsets.only(right: 2),
onTap: () { onTap: () {
setState(() { setState(() {
drawer = true; drawer = true;
}); });
}, },
child: Tooltip( child: Icon(
message: translate('Search'), Icons.search_rounded,
child: Icon( color: Theme.of(context).hintColor,
Icons.search_rounded, ));
color: Theme.of(context).hintColor,
)));
} }
Widget _buildSearchBar() { Widget _buildSearchBar() {
@@ -697,13 +700,13 @@ class _PeerSearchBarState extends State<PeerSearchBar> {
baseOffset: 0, baseOffset: 0,
extentOffset: peerSearchTextController.value.text.length); extentOffset: peerSearchTextController.value.text.length);
}); });
return Container( return Obx(() => Container(
width: isMobile ? 120 : 140, width: stateGlobal.isPortrait.isTrue ? 120 : 140,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Theme.of(context).colorScheme.background, color: Theme.of(context).colorScheme.background,
borderRadius: BorderRadius.circular(6), borderRadius: BorderRadius.circular(6),
), ),
child: Obx(() => Row( child: Row(
children: [ children: [
Expanded( Expanded(
child: Row( child: Row(
@@ -764,8 +767,8 @@ class _PeerSearchBarState extends State<PeerSearchBar> {
), ),
) )
], ],
)), ),
); ));
} }
} }
@@ -800,25 +803,38 @@ class _PeerViewDropdownState extends State<PeerViewDropdown> {
child: SizedBox( child: SizedBox(
height: 36, height: 36,
child: getRadio<PeerUiType>( child: getRadio<PeerUiType>(
Text( Tooltip(
translate(types.indexOf(e) == 0 message: translate(types.indexOf(e) == 0
? 'Big tiles' ? 'Big tiles'
: types.indexOf(e) == 1 : types.indexOf(e) == 1
? 'Small tiles' ? 'Small tiles'
: 'List'), : 'List'),
style: style), child: Icon(
e == PeerUiType.grid
? Icons.grid_view_rounded
: e == PeerUiType.list
? Icons.view_list_rounded
: Icons.view_agenda_rounded,
size: 18,
)),
e, e,
peerCardUiType.value, peerCardUiType.value,
dense: true, (PeerUiType? v) async { dense: true,
if (v != null) { isOptionFixed(kOptionPeerCardUiType)
peerCardUiType.value = v; ? null
setState(() {}); : (PeerUiType? v) async {
await bind.setLocalFlutterOption( if (v != null) {
k: "peer-card-ui-type", peerCardUiType.value = v;
v: peerCardUiType.value.index.toString(), setState(() {});
); await bind.setLocalFlutterOption(
} k: kOptionPeerCardUiType,
}), v: peerCardUiType.value.index.toString(),
);
if (Navigator.canPop(context)) {
Navigator.pop(context);
}
}
}),
), ),
)))); ))));
} }
@@ -826,16 +842,15 @@ class _PeerViewDropdownState extends State<PeerViewDropdown> {
var menuPos = RelativeRect.fromLTRB(0, 0, 0, 0); var menuPos = RelativeRect.fromLTRB(0, 0, 0, 0);
return _hoverAction( return _hoverAction(
context: context, context: context,
child: Tooltip( toolTip: translate('Change view'),
message: translate('Change view'), child: Icon(
child: Icon( peerCardUiType.value == PeerUiType.grid
peerCardUiType.value == PeerUiType.grid ? Icons.grid_view_rounded
? Icons.grid_view_rounded : peerCardUiType.value == PeerUiType.list
: peerCardUiType.value == PeerUiType.tile ? Icons.view_list_rounded
? Icons.view_list_rounded : Icons.view_agenda_rounded,
: Icons.view_agenda_rounded, size: 18,
size: 18, ),
)),
onTapDown: (details) { onTapDown: (details) {
final x = details.globalPosition.dx; final x = details.globalPosition.dx;
final y = details.globalPosition.dy; final y = details.globalPosition.dy;
@@ -858,16 +873,18 @@ class PeerSortDropdown extends StatefulWidget {
} }
class _PeerSortDropdownState extends State<PeerSortDropdown> { class _PeerSortDropdownState extends State<PeerSortDropdown> {
@override _PeerSortDropdownState() {
void initState() {
if (!PeerSortType.values.contains(peerSort.value)) { if (!PeerSortType.values.contains(peerSort.value)) {
peerSort.value = PeerSortType.remoteId; _loadLocalOptions();
bind.setLocalFlutterOption(
k: "peer-sorting",
v: peerSort.value,
);
} }
super.initState(); }
void _loadLocalOptions() {
peerSort.value = PeerSortType.remoteId;
bind.setLocalFlutterOption(
k: kOptionPeerSorting,
v: peerSort.value,
);
} }
@override @override
@@ -893,7 +910,7 @@ class _PeerSortDropdownState extends State<PeerSortDropdown> {
if (v != null) { if (v != null) {
peerSort.value = v; peerSort.value = v;
await bind.setLocalFlutterOption( await bind.setLocalFlutterOption(
k: "peer-sorting", k: kOptionPeerSorting,
v: peerSort.value, v: peerSort.value,
); );
} }
@@ -905,12 +922,11 @@ class _PeerSortDropdownState extends State<PeerSortDropdown> {
var menuPos = RelativeRect.fromLTRB(0, 0, 0, 0); var menuPos = RelativeRect.fromLTRB(0, 0, 0, 0);
return _hoverAction( return _hoverAction(
context: context, context: context,
child: Tooltip( toolTip: translate('Sort by'),
message: translate('Sort by'), child: Icon(
child: Icon( Icons.sort_rounded,
Icons.sort_rounded, size: 18,
size: 18, ),
)),
onTapDown: (details) { onTapDown: (details) {
final x = details.globalPosition.dx; final x = details.globalPosition.dx;
final y = details.globalPosition.dy; final y = details.globalPosition.dy;
@@ -992,6 +1008,7 @@ Widget _hoverAction(
{required BuildContext context, {required BuildContext context,
required Widget child, required Widget child,
required Function() onTap, required Function() onTap,
required String toolTip,
GestureTapDownCallback? onTapDown, GestureTapDownCallback? onTapDown,
RxBool? hoverableWhenfalse, RxBool? hoverableWhenfalse,
EdgeInsetsGeometry padding = const EdgeInsets.all(4.0)}) { EdgeInsetsGeometry padding = const EdgeInsets.all(4.0)}) {
@@ -1000,16 +1017,19 @@ Widget _hoverAction(
color: Theme.of(context).colorScheme.background, color: Theme.of(context).colorScheme.background,
borderRadius: BorderRadius.circular(6), borderRadius: BorderRadius.circular(6),
); );
return Obx( return Tooltip(
() => Container( message: toolTip,
margin: EdgeInsets.symmetric(horizontal: 1), child: Obx(
decoration: () => Container(
(hover.value || hoverableWhenfalse?.value == false) ? deco : null, margin: EdgeInsets.symmetric(horizontal: 1),
child: InkWell( decoration:
onHover: (value) => hover.value = value, (hover.value || hoverableWhenfalse?.value == false) ? deco : null,
onTap: onTap, child: InkWell(
onTapDown: onTapDown, onHover: (value) => hover.value = value,
child: Container(padding: padding, child: child))), onTap: onTap,
onTapDown: onTapDown,
child: Container(padding: padding, child: child))),
),
); );
} }

View File

@@ -4,12 +4,14 @@ import 'dart:collection';
import 'package:dynamic_layouts/dynamic_layouts.dart'; import 'package:dynamic_layouts/dynamic_layouts.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/desktop/widgets/scroll_wrapper.dart'; import 'package:flutter_hbb/desktop/widgets/scroll_wrapper.dart';
import 'package:flutter_hbb/models/peer_tab_model.dart';
import 'package:flutter_hbb/models/state_model.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:visibility_detector/visibility_detector.dart'; import 'package:visibility_detector/visibility_detector.dart';
import 'package:window_manager/window_manager.dart'; import 'package:window_manager/window_manager.dart';
import 'package:flutter_hbb/models/peer_tab_model.dart';
import '../../common.dart'; import '../../common.dart';
import '../../models/peer_model.dart'; import '../../models/peer_model.dart';
@@ -41,14 +43,26 @@ class LoadEvent {
static const String group = 'load_group_peers'; static const String group = 'load_group_peers';
} }
class PeersModelName {
static const String recent = 'recent peer';
static const String favorite = 'fav peer';
static const String lan = 'discovered peer';
static const String addressBook = 'address book peer';
static const String group = 'group peer';
}
/// for peer search text, global obs value /// for peer search text, global obs value
final peerSearchText = "".obs; final peerSearchText = "".obs;
/// for peer sort, global obs value /// for peer sort, global obs value
final peerSort = bind.getLocalFlutterOption(k: 'peer-sorting').obs; RxString? _peerSort;
RxString get peerSort {
_peerSort ??= bind.getLocalFlutterOption(k: kOptionPeerSorting).obs;
return _peerSort!;
}
// list for listener // list for listener
final obslist = [peerSearchText, peerSort].obs; RxList<RxString> get obslist => [peerSearchText, peerSort].obs;
final peerSearchTextController = final peerSearchTextController =
TextEditingController(text: peerSearchText.value); TextEditingController(text: peerSearchText.value);
@@ -70,7 +84,8 @@ class _PeersView extends StatefulWidget {
} }
/// State for the peer widget. /// State for the peer widget.
class _PeersViewState extends State<_PeersView> with WindowListener { class _PeersViewState extends State<_PeersView>
with WindowListener, WidgetsBindingObserver {
static const int _maxQueryCount = 3; static const int _maxQueryCount = 3;
final HashMap<String, String> _emptyMessages = HashMap.from({ final HashMap<String, String> _emptyMessages = HashMap.from({
LoadEvent.recent: 'empty_recent_tip', LoadEvent.recent: 'empty_recent_tip',
@@ -78,24 +93,15 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
LoadEvent.lan: 'empty_lan_tip', LoadEvent.lan: 'empty_lan_tip',
LoadEvent.addressBook: 'empty_address_book_tip', LoadEvent.addressBook: 'empty_address_book_tip',
}); });
final space = isDesktop ? 12.0 : 8.0; final space = (isDesktop || isWebDesktop) ? 12.0 : 8.0;
final _curPeers = <String>{}; final _curPeers = <String>{};
var _lastChangeTime = DateTime.now(); var _lastChangeTime = DateTime.now();
var _lastQueryPeers = <String>{}; var _lastQueryPeers = <String>{};
var _lastQueryTime = DateTime.now().add(const Duration(seconds: 30)); var _lastQueryTime = DateTime.now();
var _lastWindowRestoreTime = DateTime.now();
var _queryCount = 0; var _queryCount = 0;
var _exit = false; var _exit = false;
bool _isActive = true;
late final mobileWidth = () {
const minWidth = 320.0;
final windowWidth = MediaQuery.of(context).size.width;
var width = windowWidth - 2 * space;
if (windowWidth > minWidth + 2 * space) {
final n = (windowWidth / (minWidth + 2 * space)).floor();
width = windowWidth / n - 2 * space;
}
return width;
}();
final _scrollController = ScrollController(); final _scrollController = ScrollController();
@@ -106,12 +112,14 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
@override @override
void initState() { void initState() {
windowManager.addListener(this); windowManager.addListener(this);
WidgetsBinding.instance.addObserver(this);
super.initState(); super.initState();
} }
@override @override
void dispose() { void dispose() {
windowManager.removeListener(this); windowManager.removeListener(this);
WidgetsBinding.instance.removeObserver(this);
_exit = true; _exit = true;
super.dispose(); super.dispose();
} }
@@ -119,17 +127,61 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
@override @override
void onWindowFocus() { void onWindowFocus() {
_queryCount = 0; _queryCount = 0;
_isActive = true;
}
@override
void onWindowBlur() {
// We need this comparison because window restore (on Windows) also triggers `onWindowBlur()`.
// Maybe it's a bug of the window manager, but the source code seems to be correct.
//
// Although `onWindowRestore()` is called after `onWindowBlur()` in my test,
// we need the following comparison to ensure that `_isActive` is true in the end.
if (isWindows &&
DateTime.now().difference(_lastWindowRestoreTime) <
const Duration(milliseconds: 300)) {
return;
}
_queryCount = _maxQueryCount;
_isActive = false;
}
@override
void onWindowRestore() {
// Window restore (on MacOS and Linux) also triggers `onWindowFocus()`.
// But on Windows, it triggers `onWindowBlur()`, mybe it's a bug of the window manager.
if (!isWindows) return;
_queryCount = 0;
_isActive = true;
_lastWindowRestoreTime = DateTime.now();
} }
@override @override
void onWindowMinimize() { void onWindowMinimize() {
_queryCount = _maxQueryCount; // Window minimize also triggers `onWindowBlur()`.
}
// This function is required for mobile.
// `onWindowFocus` works fine for desktop.
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
if (isDesktop || isWebDesktop) return;
if (state == AppLifecycleState.resumed) {
_isActive = true;
_queryCount = 0;
} else if (state == AppLifecycleState.inactive) {
_isActive = false;
}
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ChangeNotifierProvider<Peers>( // We should avoid too many rebuilds. MacOS(m1, 14.6.1) on Flutter 3.19.6.
create: (context) => widget.peers, // Continious rebuilds of `ChangeNotifierProvider` will cause memory leak.
// Simple demo can reproduce this issue.
return ChangeNotifierProvider<Peers>.value(
value: widget.peers,
child: Consumer<Peers>(builder: (context, peers, child) { child: Consumer<Peers>(builder: (context, peers, child) {
if (peers.peers.isEmpty) { if (peers.peers.isEmpty) {
gFFI.peerTabModel.setCurrentTabCachedPeers([]); gFFI.peerTabModel.setCurrentTabCachedPeers([]);
@@ -183,61 +235,68 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
var peers = snapshot.data!; var peers = snapshot.data!;
if (peers.length > 1000) peers = peers.sublist(0, 1000); if (peers.length > 1000) peers = peers.sublist(0, 1000);
gFFI.peerTabModel.setCurrentTabCachedPeers(peers); gFFI.peerTabModel.setCurrentTabCachedPeers(peers);
buildOnePeer(Peer peer) { buildOnePeer(Peer peer, bool isPortrait) {
final visibilityChild = VisibilityDetector( final visibilityChild = VisibilityDetector(
key: ValueKey(_cardId(peer.id)), key: ValueKey(_cardId(peer.id)),
onVisibilityChanged: onVisibilityChanged, onVisibilityChanged: onVisibilityChanged,
child: widget.peerCardBuilder(peer), child: widget.peerCardBuilder(peer),
); );
final windowWidth = MediaQuery.of(context).size.width;
// `Provider.of<PeerTabModel>(context)` will causes infinete loop. // `Provider.of<PeerTabModel>(context)` will causes infinete loop.
// Because `gFFI.peerTabModel.setCurrentTabCachedPeers(peers)` will trigger `notifyListeners()`. // Because `gFFI.peerTabModel.setCurrentTabCachedPeers(peers)` will trigger `notifyListeners()`.
// //
// No need to listen the currentTab change event. // No need to listen the currentTab change event.
// Because the currentTab change event will trigger the peers change event, // Because the currentTab change event will trigger the peers change event,
// and the peers change event will trigger _buildPeersView(). // and the peers change event will trigger _buildPeersView().
final currentTab = Provider.of<PeerTabModel>(context, listen: false).currentTab; return !isPortrait
final hideAbTagsPanel = bind.mainGetLocalOption(key: "hideAbTagsPanel").isNotEmpty; ? Obx(() => peerCardUiType.value == PeerUiType.list
return isDesktop ? Container(height: 45, child: visibilityChild)
? Obx( : peerCardUiType.value == PeerUiType.grid
() => SizedBox( ? SizedBox(
width: peerCardUiType.value != PeerUiType.list width: 220, height: 140, child: visibilityChild)
? 220 : SizedBox(
: currentTab == PeerTabIndex.group.index || (currentTab == PeerTabIndex.ab.index && !hideAbTagsPanel) width: 220, height: 42, child: visibilityChild))
? windowWidth - 390 : : Container(child: visibilityChild);
windowWidth - 227,
height:
peerCardUiType.value == PeerUiType.grid ? 140 : peerCardUiType.value != PeerUiType.list ? 42 : 45,
child: visibilityChild,
),
)
: SizedBox(width: mobileWidth, child: visibilityChild);
} }
final Widget child; // We should avoid too many rebuilds. Win10(Some machines) on Flutter 3.19.6.
if (isMobile) { // Continious rebuilds of `ListView.builder` will cause memory leak.
child = DynamicGridView.builder( // Simple demo can reproduce this issue.
gridDelegate: SliverGridDelegateWithWrapping( final Widget child = Obx(() => stateGlobal.isPortrait.isTrue
mainAxisSpacing: space / 2, crossAxisSpacing: space), ? ListView.builder(
itemCount: peers.length,
itemBuilder: (BuildContext context, int index) {
return buildOnePeer(peers[index]);
},
);
} else {
child = DesktopScrollWrapper(
scrollController: _scrollController,
child: DynamicGridView.builder(
controller: _scrollController,
physics: DraggableNeverScrollableScrollPhysics(),
gridDelegate: SliverGridDelegateWithWrapping(
mainAxisSpacing: space / 2, crossAxisSpacing: space),
itemCount: peers.length, itemCount: peers.length,
itemBuilder: (BuildContext context, int index) { itemBuilder: (BuildContext context, int index) {
return buildOnePeer(peers[index]); return buildOnePeer(peers[index], true).marginOnly(
}), top: index == 0 ? 0 : space / 2, bottom: space / 2);
); },
} )
: peerCardUiType.value == PeerUiType.list
? DesktopScrollWrapper(
scrollController: _scrollController,
child: ListView.builder(
controller: _scrollController,
physics: DraggableNeverScrollableScrollPhysics(),
itemCount: peers.length,
itemBuilder: (BuildContext context, int index) {
return buildOnePeer(peers[index], false)
.marginOnly(
right: space,
top: index == 0 ? 0 : space / 2,
bottom: space / 2);
}),
)
: DesktopScrollWrapper(
scrollController: _scrollController,
child: DynamicGridView.builder(
controller: _scrollController,
physics: DraggableNeverScrollableScrollPhysics(),
gridDelegate: SliverGridDelegateWithWrapping(
mainAxisSpacing: space / 2,
crossAxisSpacing: space),
itemCount: peers.length,
itemBuilder: (BuildContext context, int index) {
return buildOnePeer(peers[index], false);
}),
));
if (updateEvent == UpdateEvent.load) { if (updateEvent == UpdateEvent.load) {
_curPeers.clear(); _curPeers.clear();
@@ -258,10 +317,14 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
return body; return body;
} }
final _queryInterval = const Duration(seconds: 20); var _queryInterval = const Duration(seconds: 20);
void _startCheckOnlines() { void _startCheckOnlines() {
() async { () async {
final p = await bind.mainIsUsingPublicServer();
if (!p) {
_queryInterval = const Duration(seconds: 6);
}
while (!_exit) { while (!_exit) {
final now = DateTime.now(); final now = DateTime.now();
if (!setEquals(_curPeers, _lastQueryPeers)) { if (!setEquals(_curPeers, _lastQueryPeers)) {
@@ -269,7 +332,12 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
_queryOnlines(false); _queryOnlines(false);
} }
} else { } else {
if (_queryCount < _maxQueryCount) { final skipIfIsWeb =
isWeb && !(stateGlobal.isWebVisible && stateGlobal.isInMainPage);
final skipIfMobile =
(isAndroid || isIOS) && !stateGlobal.isInMainPage;
final skipIfNotActive = skipIfIsWeb || skipIfMobile || !_isActive;
if (!skipIfNotActive && (_queryCount < _maxQueryCount || !p)) {
if (now.difference(_lastQueryTime) >= _queryInterval) { if (now.difference(_lastQueryTime) >= _queryInterval) {
if (_curPeers.isNotEmpty) { if (_curPeers.isNotEmpty) {
bind.queryOnlines(ids: _curPeers.toList(growable: false)); bind.queryOnlines(ids: _curPeers.toList(growable: false));
@@ -287,14 +355,14 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
_queryOnlines(bool isLoadEvent) { _queryOnlines(bool isLoadEvent) {
if (_curPeers.isNotEmpty) { if (_curPeers.isNotEmpty) {
bind.queryOnlines(ids: _curPeers.toList(growable: false)); bind.queryOnlines(ids: _curPeers.toList(growable: false));
_lastQueryPeers = {..._curPeers};
if (isLoadEvent) {
_lastChangeTime = DateTime.now();
} else {
_lastQueryTime = DateTime.now().subtract(_queryInterval);
}
_queryCount = 0; _queryCount = 0;
} }
_lastQueryPeers = {..._curPeers};
if (isLoadEvent) {
_lastChangeTime = DateTime.now();
} else {
_lastQueryTime = DateTime.now().subtract(_queryInterval);
}
} }
Future<List<Peer>>? matchPeers( Future<List<Peer>>? matchPeers(
@@ -307,7 +375,7 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
if (!PeerSortType.values.contains(sortedBy)) { if (!PeerSortType.values.contains(sortedBy)) {
sortedBy = PeerSortType.remoteId; sortedBy = PeerSortType.remoteId;
bind.setLocalFlutterOption( bind.setLocalFlutterOption(
k: "peer-sorting", k: kOptionPeerSorting,
v: sortedBy, v: sortedBy,
); );
} }
@@ -350,27 +418,39 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
} }
abstract class BasePeersView extends StatelessWidget { abstract class BasePeersView extends StatelessWidget {
final String name; final PeerTabIndex peerTabIndex;
final String loadEvent;
final PeerFilter? peerFilter; final PeerFilter? peerFilter;
final PeerCardBuilder peerCardBuilder; final PeerCardBuilder peerCardBuilder;
final RxList<Peer>? initPeers;
const BasePeersView({ const BasePeersView({
Key? key, Key? key,
required this.name, required this.peerTabIndex,
required this.loadEvent,
this.peerFilter, this.peerFilter,
required this.peerCardBuilder, required this.peerCardBuilder,
required this.initPeers,
}) : super(key: key); }) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
Peers peers;
switch (peerTabIndex) {
case PeerTabIndex.recent:
peers = gFFI.recentPeersModel;
break;
case PeerTabIndex.fav:
peers = gFFI.favoritePeersModel;
break;
case PeerTabIndex.lan:
peers = gFFI.lanPeersModel;
break;
case PeerTabIndex.ab:
peers = gFFI.abModel.peersModel;
break;
case PeerTabIndex.group:
peers = gFFI.groupModel.peersModel;
break;
}
return _PeersView( return _PeersView(
peers: Peers(name: name, loadEvent: loadEvent, initPeers: initPeers), peers: peers, peerFilter: peerFilter, peerCardBuilder: peerCardBuilder);
peerFilter: peerFilter,
peerCardBuilder: peerCardBuilder);
} }
} }
@@ -379,13 +459,11 @@ class RecentPeersView extends BasePeersView {
{Key? key, EdgeInsets? menuPadding, ScrollController? scrollController}) {Key? key, EdgeInsets? menuPadding, ScrollController? scrollController})
: super( : super(
key: key, key: key,
name: 'recent peer', peerTabIndex: PeerTabIndex.recent,
loadEvent: LoadEvent.recent,
peerCardBuilder: (Peer peer) => RecentPeerCard( peerCardBuilder: (Peer peer) => RecentPeerCard(
peer: peer, peer: peer,
menuPadding: menuPadding, menuPadding: menuPadding,
), ),
initPeers: null,
); );
@override @override
@@ -401,13 +479,11 @@ class FavoritePeersView extends BasePeersView {
{Key? key, EdgeInsets? menuPadding, ScrollController? scrollController}) {Key? key, EdgeInsets? menuPadding, ScrollController? scrollController})
: super( : super(
key: key, key: key,
name: 'favorite peer', peerTabIndex: PeerTabIndex.fav,
loadEvent: LoadEvent.favorite,
peerCardBuilder: (Peer peer) => FavoritePeerCard( peerCardBuilder: (Peer peer) => FavoritePeerCard(
peer: peer, peer: peer,
menuPadding: menuPadding, menuPadding: menuPadding,
), ),
initPeers: null,
); );
@override @override
@@ -423,13 +499,11 @@ class DiscoveredPeersView extends BasePeersView {
{Key? key, EdgeInsets? menuPadding, ScrollController? scrollController}) {Key? key, EdgeInsets? menuPadding, ScrollController? scrollController})
: super( : super(
key: key, key: key,
name: 'discovered peer', peerTabIndex: PeerTabIndex.lan,
loadEvent: LoadEvent.lan,
peerCardBuilder: (Peer peer) => DiscoveredPeerCard( peerCardBuilder: (Peer peer) => DiscoveredPeerCard(
peer: peer, peer: peer,
menuPadding: menuPadding, menuPadding: menuPadding,
), ),
initPeers: null,
); );
@override @override
@@ -442,21 +516,16 @@ class DiscoveredPeersView extends BasePeersView {
class AddressBookPeersView extends BasePeersView { class AddressBookPeersView extends BasePeersView {
AddressBookPeersView( AddressBookPeersView(
{Key? key, {Key? key, EdgeInsets? menuPadding, ScrollController? scrollController})
EdgeInsets? menuPadding,
ScrollController? scrollController,
required RxList<Peer> initPeers})
: super( : super(
key: key, key: key,
name: 'address book peer', peerTabIndex: PeerTabIndex.ab,
loadEvent: LoadEvent.addressBook,
peerFilter: (Peer peer) => peerFilter: (Peer peer) =>
_hitTag(gFFI.abModel.selectedTags, peer.tags), _hitTag(gFFI.abModel.selectedTags, peer.tags),
peerCardBuilder: (Peer peer) => AddressBookPeerCard( peerCardBuilder: (Peer peer) => AddressBookPeerCard(
peer: peer, peer: peer,
menuPadding: menuPadding, menuPadding: menuPadding,
), ),
initPeers: initPeers,
); );
static bool _hitTag(List<dynamic> selectedTags, List<dynamic> idents) { static bool _hitTag(List<dynamic> selectedTags, List<dynamic> idents) {
@@ -483,20 +552,15 @@ class AddressBookPeersView extends BasePeersView {
class MyGroupPeerView extends BasePeersView { class MyGroupPeerView extends BasePeersView {
MyGroupPeerView( MyGroupPeerView(
{Key? key, {Key? key, EdgeInsets? menuPadding, ScrollController? scrollController})
EdgeInsets? menuPadding,
ScrollController? scrollController,
required RxList<Peer> initPeers})
: super( : super(
key: key, key: key,
name: 'group peer', peerTabIndex: PeerTabIndex.group,
loadEvent: LoadEvent.group,
peerFilter: filter, peerFilter: filter,
peerCardBuilder: (Peer peer) => MyGroupPeerCard( peerCardBuilder: (Peer peer) => MyGroupPeerCard(
peer: peer, peer: peer,
menuPadding: menuPadding, menuPadding: menuPadding,
), ),
initPeers: initPeers,
); );
static bool filter(Peer peer) { static bool filter(Peer peer) {

View File

@@ -27,6 +27,10 @@ class RawKeyFocusScope extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// https://github.com/flutter/flutter/issues/154053
final useRawKeyEvents = isLinux && !isWeb;
// FIXME: On Windows, `AltGr` will generate `Alt` and `Control` key events,
// while `Alt` and `Control` are seperated key events for en-US input method.
return FocusScope( return FocusScope(
autofocus: true, autofocus: true,
child: Focus( child: Focus(
@@ -34,8 +38,14 @@ class RawKeyFocusScope extends StatelessWidget {
canRequestFocus: true, canRequestFocus: true,
focusNode: focusNode, focusNode: focusNode,
onFocusChange: onFocusChange, onFocusChange: onFocusChange,
onKey: (FocusNode data, RawKeyEvent e) => onKey: useRawKeyEvents
inputModel.handleRawKeyEvent(e), ? (FocusNode data, RawKeyEvent event) =>
inputModel.handleRawKeyEvent(event)
: null,
onKeyEvent: useRawKeyEvents
? null
: (FocusNode node, KeyEvent event) =>
inputModel.handleKeyEvent(event),
child: child)); child: child));
} }
} }
@@ -69,6 +79,8 @@ class RawTouchGestureDetectorRegion extends StatefulWidget {
class _RawTouchGestureDetectorRegionState class _RawTouchGestureDetectorRegionState
extends State<RawTouchGestureDetectorRegion> { extends State<RawTouchGestureDetectorRegion> {
Offset _cacheLongPressPosition = Offset(0, 0); Offset _cacheLongPressPosition = Offset(0, 0);
// Timestamp of the last long press event.
int _cacheLongPressPositionTs = 0;
double _mouseScrollIntegral = 0; // mouse scroll speed controller double _mouseScrollIntegral = 0; // mouse scroll speed controller
double _scale = 1; double _scale = 1;
@@ -77,7 +89,7 @@ class _RawTouchGestureDetectorRegionState
FFI get ffi => widget.ffi; FFI get ffi => widget.ffi;
FfiModel get ffiModel => widget.ffiModel; FfiModel get ffiModel => widget.ffiModel;
InputModel get inputModel => widget.inputModel; InputModel get inputModel => widget.inputModel;
bool get handleTouch => isDesktop || ffiModel.touchMode; bool get handleTouch => (isDesktop || isWebDesktop) || ffiModel.touchMode;
SessionID get sessionId => ffi.sessionId; SessionID get sessionId => ffi.sessionId;
@override @override
@@ -95,8 +107,9 @@ class _RawTouchGestureDetectorRegionState
} }
if (handleTouch) { if (handleTouch) {
// Desktop or mobile "Touch mode" // Desktop or mobile "Touch mode"
ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy); if (ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy)) {
inputModel.tapDown(MouseButtons.left); inputModel.tapDown(MouseButtons.left);
}
} }
} }
@@ -105,8 +118,9 @@ class _RawTouchGestureDetectorRegionState
return; return;
} }
if (handleTouch) { if (handleTouch) {
ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy); if (ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy)) {
inputModel.tapUp(MouseButtons.left); inputModel.tapUp(MouseButtons.left);
}
} }
} }
@@ -134,6 +148,9 @@ class _RawTouchGestureDetectorRegionState
if (lastDeviceKind != PointerDeviceKind.touch) { if (lastDeviceKind != PointerDeviceKind.touch) {
return; return;
} }
if (ffiModel.touchMode && ffi.cursorModel.lastIsBlocked) {
return;
}
inputModel.tap(MouseButtons.left); inputModel.tap(MouseButtons.left);
inputModel.tap(MouseButtons.left); inputModel.tap(MouseButtons.left);
} }
@@ -146,6 +163,7 @@ class _RawTouchGestureDetectorRegionState
if (handleTouch) { if (handleTouch) {
ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy); ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
_cacheLongPressPosition = d.localPosition; _cacheLongPressPosition = d.localPosition;
_cacheLongPressPositionTs = DateTime.now().millisecondsSinceEpoch;
} }
} }
@@ -183,7 +201,7 @@ class _RawTouchGestureDetectorRegionState
if (lastDeviceKind != PointerDeviceKind.touch) { if (lastDeviceKind != PointerDeviceKind.touch) {
return; return;
} }
if (isDesktop || !ffiModel.touchMode) { if ((isDesktop || isWebDesktop) || !ffiModel.touchMode) {
inputModel.tap(MouseButtons.right); inputModel.tap(MouseButtons.right);
} }
} }
@@ -203,7 +221,7 @@ class _RawTouchGestureDetectorRegionState
return; return;
} }
if (!handleTouch) { if (!handleTouch) {
ffi.cursorModel.updatePan(d.delta.dx, d.delta.dy, handleTouch); ffi.cursorModel.updatePan(d.delta, d.localPosition, handleTouch);
} }
} }
@@ -222,6 +240,22 @@ class _RawTouchGestureDetectorRegionState
return; return;
} }
if (handleTouch) { if (handleTouch) {
if (ffi.cursorModel.shouldBlock(d.localPosition.dx, d.localPosition.dy)) {
return;
}
if (isDesktop || isWebDesktop) {
ffi.cursorModel.trySetRemoteWindowCoords();
}
// Workaround for the issue that the first pan event is sent a long time after the start event.
// If the time interval between the start event and the first pan event is less than 500ms,
// we consider to use the long press position as the start position.
//
// TODO: We should find a better way to send the first pan event as soon as possible.
if (DateTime.now().millisecondsSinceEpoch - _cacheLongPressPositionTs <
500) {
ffi.cursorModel
.move(_cacheLongPressPosition.dx, _cacheLongPressPosition.dy);
}
inputModel.sendMouse('down', MouseButtons.left); inputModel.sendMouse('down', MouseButtons.left);
ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy); ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
} else { } else {
@@ -241,13 +275,19 @@ class _RawTouchGestureDetectorRegionState
if (lastDeviceKind != PointerDeviceKind.touch) { if (lastDeviceKind != PointerDeviceKind.touch) {
return; return;
} }
ffi.cursorModel.updatePan(d.delta.dx, d.delta.dy, handleTouch); if (ffi.cursorModel.shouldBlock(d.localPosition.dx, d.localPosition.dy)) {
return;
}
ffi.cursorModel.updatePan(d.delta, d.localPosition, handleTouch);
} }
onOneFingerPanEnd(DragEndDetails d) { onOneFingerPanEnd(DragEndDetails d) {
if (lastDeviceKind != PointerDeviceKind.touch) { if (lastDeviceKind != PointerDeviceKind.touch) {
return; return;
} }
if (isDesktop || isWebDesktop) {
ffi.cursorModel.clearRemoteWindowCoords();
}
inputModel.sendMouse('up', MouseButtons.left); inputModel.sendMouse('up', MouseButtons.left);
} }
@@ -262,7 +302,7 @@ class _RawTouchGestureDetectorRegionState
if (lastDeviceKind != PointerDeviceKind.touch) { if (lastDeviceKind != PointerDeviceKind.touch) {
return; return;
} }
if (isDesktop) { if ((isDesktop || isWebDesktop)) {
final scale = ((d.scale - _scale) * 1000).toInt(); final scale = ((d.scale - _scale) * 1000).toInt();
_scale = d.scale; _scale = d.scale;
@@ -275,7 +315,7 @@ class _RawTouchGestureDetectorRegionState
} }
} else { } else {
// mobile // mobile
ffi.canvasModel.updateScale(d.scale / _scale); ffi.canvasModel.updateScale(d.scale / _scale, d.focalPoint);
_scale = d.scale; _scale = d.scale;
ffi.canvasModel.panX(d.focalPointDelta.dx); ffi.canvasModel.panX(d.focalPointDelta.dx);
ffi.canvasModel.panY(d.focalPointDelta.dy); ffi.canvasModel.panY(d.focalPointDelta.dy);
@@ -286,7 +326,7 @@ class _RawTouchGestureDetectorRegionState
if (lastDeviceKind != PointerDeviceKind.touch) { if (lastDeviceKind != PointerDeviceKind.touch) {
return; return;
} }
if (isDesktop) { if ((isDesktop || isWebDesktop)) {
bind.sessionSendPointer( bind.sessionSendPointer(
sessionId: sessionId, sessionId: sessionId,
msg: json.encode( msg: json.encode(
@@ -409,7 +449,9 @@ class RawPointerMouseRegion extends StatelessWidget {
onPointerPanZoomUpdate: inputModel.onPointerPanZoomUpdate, onPointerPanZoomUpdate: inputModel.onPointerPanZoomUpdate,
onPointerPanZoomEnd: inputModel.onPointerPanZoomEnd, onPointerPanZoomEnd: inputModel.onPointerPanZoomEnd,
child: MouseRegion( child: MouseRegion(
cursor: cursor ?? MouseCursor.defer, cursor: inputModel.isViewOnly
? MouseCursor.defer
: (cursor ?? MouseCursor.defer),
onEnter: onEnter, onEnter: onEnter,
onExit: onExit, onExit: onExit,
child: child, child: child,

View File

@@ -3,15 +3,14 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/models/desktop_render_texture.dart';
import 'package:flutter_hbb/models/platform_model.dart'; import 'package:flutter_hbb/models/platform_model.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
customImageQualityWidget( customImageQualityWidget(
{required double initQuality, {required double initQuality,
required double initFps, required double initFps,
required Function(double) setQuality, required Function(double)? setQuality,
required Function(double) setFps, required Function(double)? setFps,
required bool showFps, required bool showFps,
required bool showMoreQuality}) { required bool showMoreQuality}) {
if (initQuality < kMinQuality || if (initQuality < kMinQuality ||
@@ -27,16 +26,12 @@ customImageQualityWidget(
final RxBool moreQualityChecked = RxBool(qualityValue.value > kMaxQuality); final RxBool moreQualityChecked = RxBool(qualityValue.value > kMaxQuality);
final debouncerQuality = Debouncer<double>( final debouncerQuality = Debouncer<double>(
Duration(milliseconds: 1000), Duration(milliseconds: 1000),
onChanged: (double v) { onChanged: setQuality,
setQuality(v);
},
initialValue: qualityValue.value, initialValue: qualityValue.value,
); );
final debouncerFps = Debouncer<double>( final debouncerFps = Debouncer<double>(
Duration(milliseconds: 1000), Duration(milliseconds: 1000),
onChanged: (double v) { onChanged: setFps,
setFps(v);
},
initialValue: fpsValue.value, initialValue: fpsValue.value,
); );
@@ -62,10 +57,12 @@ customImageQualityWidget(
divisions: moreQualityChecked.value divisions: moreQualityChecked.value
? ((kMaxMoreQuality - kMinQuality) / 10).round() ? ((kMaxMoreQuality - kMinQuality) / 10).round()
: ((kMaxQuality - kMinQuality) / 5).round(), : ((kMaxQuality - kMinQuality) / 5).round(),
onChanged: (double value) async { onChanged: setQuality == null
qualityValue.value = value; ? null
debouncerQuality.value = value; : (double value) async {
}, qualityValue.value = value;
debouncerQuality.value = value;
},
), ),
), ),
Expanded( Expanded(
@@ -124,10 +121,12 @@ customImageQualityWidget(
min: kMinFps, min: kMinFps,
max: kMaxFps, max: kMaxFps,
divisions: ((kMaxFps - kMinFps) / 5).round(), divisions: ((kMaxFps - kMinFps) / 5).round(),
onChanged: (double value) async { onChanged: setFps == null
fpsValue.value = value; ? null
debouncerFps.value = value; : (double value) async {
}, fpsValue.value = value;
debouncerFps.value = value;
},
), ),
), ),
Expanded( Expanded(
@@ -152,21 +151,29 @@ customImageQualitySetting() {
final qualityKey = 'custom_image_quality'; final qualityKey = 'custom_image_quality';
final fpsKey = 'custom-fps'; final fpsKey = 'custom-fps';
var initQuality = final initQuality =
(double.tryParse(bind.mainGetUserDefaultOption(key: qualityKey)) ?? (double.tryParse(bind.mainGetUserDefaultOption(key: qualityKey)) ??
kDefaultQuality); kDefaultQuality);
var initFps = (double.tryParse(bind.mainGetUserDefaultOption(key: fpsKey)) ?? final isQuanlityFixed = isOptionFixed(qualityKey);
kDefaultFps); final initFps =
(double.tryParse(bind.mainGetUserDefaultOption(key: fpsKey)) ??
kDefaultFps);
final isFpsFixed = isOptionFixed(fpsKey);
return customImageQualityWidget( return customImageQualityWidget(
initQuality: initQuality, initQuality: initQuality,
initFps: initFps, initFps: initFps,
setQuality: (v) { setQuality: isQuanlityFixed
bind.mainSetUserDefaultOption(key: qualityKey, value: v.toString()); ? null
}, : (v) {
setFps: (v) { bind.mainSetUserDefaultOption(
bind.mainSetUserDefaultOption(key: fpsKey, value: v.toString()); key: qualityKey, value: v.toString());
}, },
setFps: isFpsFixed
? null
: (v) {
bind.mainSetUserDefaultOption(key: fpsKey, value: v.toString());
},
showFps: true, showFps: true,
showMoreQuality: true); showMoreQuality: true);
} }
@@ -208,27 +215,31 @@ List<Widget> ServerConfigImportExportWidgets(
List<(String, String)> otherDefaultSettings() { List<(String, String)> otherDefaultSettings() {
List<(String, String)> v = [ List<(String, String)> v = [
('View Mode', 'view_only'), ('View Mode', kOptionViewOnly),
if (isDesktop) ('show_monitors_tip', kKeyShowMonitorsToolbar), if ((isDesktop || isWebDesktop))
if (isDesktop) ('Collapse toolbar', 'collapse_toolbar'), ('show_monitors_tip', kKeyShowMonitorsToolbar),
('Show remote cursor', 'show_remote_cursor'), if ((isDesktop || isWebDesktop))
if (isDesktop) ('Zoom cursor', 'zoom-cursor'), ('Collapse toolbar', kOptionCollapseToolbar),
('Show quality monitor', 'show_quality_monitor'), ('Show remote cursor', kOptionShowRemoteCursor),
('Mute', 'disable_audio'), ('Follow remote cursor', kOptionFollowRemoteCursor),
if (isDesktop) ('Enable file copy and paste', 'enable_file_transfer'), ('Follow remote window focus', kOptionFollowRemoteWindow),
('Disable clipboard', 'disable_clipboard'), if ((isDesktop || isWebDesktop)) ('Zoom cursor', kOptionZoomCursor),
('Lock after session end', 'lock_after_session_end'), ('Show quality monitor', kOptionShowQualityMonitor),
('Privacy mode', 'privacy_mode'), ('Mute', kOptionDisableAudio),
if (isMobile) ('Touch mode', 'touch-mode'), if (isDesktop) ('Enable file copy and paste', kOptionEnableFileCopyPaste),
('True color (4:4:4)', 'i444'), ('Disable clipboard', kOptionDisableClipboard),
('Reverse mouse wheel', 'reverse_mouse_wheel'), ('Lock after session end', kOptionLockAfterSessionEnd),
('swap-left-right-mouse', 'swap-left-right-mouse'), ('Privacy mode', kOptionPrivacyMode),
if (isDesktop && useTextureRender) if (isMobile) ('Touch mode', kOptionTouchMode),
('True color (4:4:4)', kOptionI444),
('Reverse mouse wheel', kKeyReverseMouseWheel),
('swap-left-right-mouse', kOptionSwapLeftRightMouse),
if (isDesktop)
( (
'Show displays as individual windows', 'Show displays as individual windows',
kKeyShowDisplaysAsIndividualWindows kKeyShowDisplaysAsIndividualWindows
), ),
if (isDesktop && useTextureRender) if (isDesktop)
( (
'Use all my displays for the remote session', 'Use all my displays for the remote session',
kKeyUseAllMyDisplaysForTheRemoteSession kKeyUseAllMyDisplaysForTheRemoteSession

View File

@@ -1,5 +1,4 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
@@ -7,9 +6,9 @@ import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/common/shared_state.dart'; import 'package:flutter_hbb/common/shared_state.dart';
import 'package:flutter_hbb/common/widgets/dialog.dart'; import 'package:flutter_hbb/common/widgets/dialog.dart';
import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/desktop/widgets/remote_toolbar.dart';
import 'package:flutter_hbb/models/model.dart'; import 'package:flutter_hbb/models/model.dart';
import 'package:flutter_hbb/models/platform_model.dart'; import 'package:flutter_hbb/models/platform_model.dart';
import 'package:flutter_hbb/models/desktop_render_texture.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
bool isEditOsPassword = false; bool isEditOsPassword = false;
@@ -24,6 +23,20 @@ class TTextMenu {
required this.onPressed, required this.onPressed,
this.trailingIcon, this.trailingIcon,
this.divider = false}); this.divider = false});
Widget getChild() {
if (trailingIcon != null) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
child,
trailingIcon!,
],
);
} else {
return child;
}
}
} }
class TRadioMenu<T> { class TRadioMenu<T> {
@@ -95,7 +108,7 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
Text(translate(pi.isHeadless ? 'OS Account' : 'OS Password')), Text(translate(pi.isHeadless ? 'OS Account' : 'OS Password')),
]), ]),
trailingIcon: Transform.scale( trailingIcon: Transform.scale(
scale: isDesktop ? 0.8 : 1, scale: (isDesktop || isWebDesktop) ? 0.8 : 1,
child: IconButton( child: IconButton(
onPressed: () { onPressed: () {
if (isMobile && Navigator.canPop(context)) { if (isMobile && Navigator.canPop(context)) {
@@ -117,9 +130,9 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
); );
} }
// paste // paste
if (isMobile && perms['keyboard'] != false && perms['clipboard'] != false) { if (pi.platform != kPeerPlatformAndroid && perms['keyboard'] != false) {
v.add(TTextMenu( v.add(TTextMenu(
child: Text(translate('Paste')), child: Text(translate('Send clipboard keystrokes')),
onPressed: () async { onPressed: () async {
ClipboardData? data = await Clipboard.getData(Clipboard.kTextPlain); ClipboardData? data = await Clipboard.getData(Clipboard.kTextPlain);
if (data != null && data.text != null) { if (data != null && data.text != null) {
@@ -161,7 +174,7 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
); );
} }
// divider // divider
if (isDesktop) { if (isDesktop || isWebDesktop) {
v.add(TTextMenu(child: Offstage(), onPressed: () {}, divider: true)); v.add(TTextMenu(child: Offstage(), onPressed: () {}, divider: true));
} }
// ctrlAltDel // ctrlAltDel
@@ -230,7 +243,7 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
)); ));
} }
// record // record
if (!isDesktop && if (!(isDesktop || isWeb) &&
(ffi.recordingModel.start || (perms["recording"] != false))) { (ffi.recordingModel.start || (perms["recording"] != false))) {
v.add(TTextMenu( v.add(TTextMenu(
child: Row( child: Row(
@@ -251,7 +264,7 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
onPressed: () => ffi.recordingModel.toggle())); onPressed: () => ffi.recordingModel.toggle()));
} }
// fingerprint // fingerprint
if (!isDesktop) { if (!(isDesktop || isWebDesktop)) {
v.add(TTextMenu( v.add(TTextMenu(
child: Text(translate('Copy Fingerprint')), child: Text(translate('Copy Fingerprint')),
onPressed: () => onCopyFingerprint(FingerprintState.find(id).value), onPressed: () => onCopyFingerprint(FingerprintState.find(id).value),
@@ -328,7 +341,7 @@ Future<List<TRadioMenu<String>>> toolbarCodec(
final alternativeCodecs = final alternativeCodecs =
await bind.sessionAlternativeCodecs(sessionId: sessionId); await bind.sessionAlternativeCodecs(sessionId: sessionId);
final groupValue = await bind.sessionGetOption( final groupValue = await bind.sessionGetOption(
sessionId: sessionId, arg: 'codec-preference') ?? sessionId: sessionId, arg: kOptionCodecPreference) ??
''; '';
final List<bool> codecs = []; final List<bool> codecs = [];
try { try {
@@ -350,20 +363,25 @@ Future<List<TRadioMenu<String>>> toolbarCodec(
onChanged(String? value) async { onChanged(String? value) async {
if (value == null) return; if (value == null) return;
await bind.sessionPeerOption( await bind.sessionPeerOption(
sessionId: sessionId, name: 'codec-preference', value: value); sessionId: sessionId, name: kOptionCodecPreference, value: value);
bind.sessionChangePreferCodec(sessionId: sessionId); bind.sessionChangePreferCodec(sessionId: sessionId);
} }
TRadioMenu<String> radio(String label, String value, bool enabled) { TRadioMenu<String> radio(String label, String value, bool enabled) {
return TRadioMenu<String>( return TRadioMenu<String>(
child: Text(translate(label)), child: Text(label),
value: value, value: value,
groupValue: groupValue, groupValue: groupValue,
onChanged: enabled ? onChanged : null); onChanged: enabled ? onChanged : null);
} }
var autoLabel = translate('Auto');
if (groupValue == 'auto' &&
ffi.qualityMonitorModel.data.codecFormat != null) {
autoLabel = '$autoLabel (${ffi.qualityMonitorModel.data.codecFormat})';
}
return [ return [
radio('Auto', 'auto', true), radio(autoLabel, 'auto', true),
if (codecs[0]) radio('VP8', 'vp8', codecs[0]), if (codecs[0]) radio('VP8', 'vp8', codecs[0]),
radio('VP9', 'vp9', true), radio('VP9', 'vp9', true),
if (codecs[1]) radio('AV1', 'av1', codecs[1]), if (codecs[1]) radio('AV1', 'av1', codecs[1]),
@@ -372,12 +390,11 @@ Future<List<TRadioMenu<String>>> toolbarCodec(
]; ];
} }
Future<List<TToggleMenu>> toolbarDisplayToggle( Future<List<TToggleMenu>> toolbarCursor(
BuildContext context, String id, FFI ffi) async { BuildContext context, String id, FFI ffi) async {
List<TToggleMenu> v = []; List<TToggleMenu> v = [];
final ffiModel = ffi.ffiModel; final ffiModel = ffi.ffiModel;
final pi = ffiModel.pi; final pi = ffiModel.pi;
final perms = ffiModel.permissions;
final sessionId = ffi.sessionId; final sessionId = ffi.sessionId;
// show remote cursor // show remote cursor
@@ -385,12 +402,17 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
!ffi.canvasModel.cursorEmbedded && !ffi.canvasModel.cursorEmbedded &&
!pi.isWayland) { !pi.isWayland) {
final state = ShowRemoteCursorState.find(id); final state = ShowRemoteCursorState.find(id);
final lockState = ShowRemoteCursorLockState.find(id);
final enabled = !ffiModel.viewOnly; final enabled = !ffiModel.viewOnly;
final option = 'show-remote-cursor'; final option = 'show-remote-cursor';
if (pi.currentDisplay == kAllDisplayValue ||
bind.sessionIsMultiUiSession(sessionId: sessionId)) {
lockState.value = false;
}
v.add(TToggleMenu( v.add(TToggleMenu(
child: Text(translate('Show remote cursor')), child: Text(translate('Show remote cursor')),
value: state.value, value: state.value,
onChanged: enabled onChanged: enabled && !lockState.value
? (value) async { ? (value) async {
if (value == null) return; if (value == null) return;
await bind.sessionToggleOption( await bind.sessionToggleOption(
@@ -400,6 +422,67 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
} }
: null)); : null));
} }
// follow remote cursor
if (pi.platform != kPeerPlatformAndroid &&
!ffi.canvasModel.cursorEmbedded &&
!pi.isWayland &&
versionCmp(pi.version, "1.2.4") >= 0 &&
pi.displays.length > 1 &&
pi.currentDisplay != kAllDisplayValue &&
!bind.sessionIsMultiUiSession(sessionId: sessionId)) {
final option = 'follow-remote-cursor';
final value =
bind.sessionGetToggleOptionSync(sessionId: sessionId, arg: option);
final showCursorOption = 'show-remote-cursor';
final showCursorState = ShowRemoteCursorState.find(id);
final showCursorLockState = ShowRemoteCursorLockState.find(id);
final showCursorEnabled = bind.sessionGetToggleOptionSync(
sessionId: sessionId, arg: showCursorOption);
showCursorLockState.value = value;
if (value && !showCursorEnabled) {
await bind.sessionToggleOption(
sessionId: sessionId, value: showCursorOption);
showCursorState.value = bind.sessionGetToggleOptionSync(
sessionId: sessionId, arg: showCursorOption);
}
v.add(TToggleMenu(
child: Text(translate('Follow remote cursor')),
value: value,
onChanged: (value) async {
if (value == null) return;
await bind.sessionToggleOption(sessionId: sessionId, value: option);
value = bind.sessionGetToggleOptionSync(
sessionId: sessionId, arg: option);
showCursorLockState.value = value;
if (!showCursorEnabled) {
await bind.sessionToggleOption(
sessionId: sessionId, value: showCursorOption);
showCursorState.value = bind.sessionGetToggleOptionSync(
sessionId: sessionId, arg: showCursorOption);
}
}));
}
// follow remote window focus
if (pi.platform != kPeerPlatformAndroid &&
!ffi.canvasModel.cursorEmbedded &&
!pi.isWayland &&
versionCmp(pi.version, "1.2.4") >= 0 &&
pi.displays.length > 1 &&
pi.currentDisplay != kAllDisplayValue &&
!bind.sessionIsMultiUiSession(sessionId: sessionId)) {
final option = 'follow-remote-window';
final value =
bind.sessionGetToggleOptionSync(sessionId: sessionId, arg: option);
v.add(TToggleMenu(
child: Text(translate('Follow remote window focus')),
value: value,
onChanged: (value) async {
if (value == null) return;
await bind.sessionToggleOption(sessionId: sessionId, value: option);
value = bind.sessionGetToggleOptionSync(
sessionId: sessionId, arg: option);
}));
}
// zoom cursor // zoom cursor
final viewStyle = await bind.sessionGetViewStyle(sessionId: sessionId) ?? ''; final viewStyle = await bind.sessionGetViewStyle(sessionId: sessionId) ?? '';
if (!isMobile && if (!isMobile &&
@@ -418,6 +501,17 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
}, },
)); ));
} }
return v;
}
Future<List<TToggleMenu>> toolbarDisplayToggle(
BuildContext context, String id, FFI ffi) async {
List<TToggleMenu> v = [];
final ffiModel = ffi.ffiModel;
final pi = ffiModel.pi;
final perms = ffiModel.permissions;
final sessionId = ffi.sessionId;
// show quality monitor // show quality monitor
final option = 'show-quality-monitor'; final option = 'show-quality-monitor';
v.add(TToggleMenu( v.add(TToggleMenu(
@@ -442,20 +536,27 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
child: Text(translate('Mute')))); child: Text(translate('Mute'))));
} }
// file copy and paste // file copy and paste
// If the version is less than 1.2.4, file copy and paste is supported on Windows only.
final isSupportIfPeer_1_2_3 = versionCmp(pi.version, '1.2.4') < 0 &&
isWindows &&
pi.platform == kPeerPlatformWindows;
// If the version is 1.2.4 or later, file copy and paste is supported when kPlatformAdditionsHasFileClipboard is set.
final isSupportIfPeer_1_2_4 = versionCmp(pi.version, '1.2.4') >= 0 &&
bind.mainHasFileClipboard() &&
pi.platformAdditions.containsKey(kPlatformAdditionsHasFileClipboard);
if (ffiModel.keyboard && if (ffiModel.keyboard &&
perms['file'] != false && perms['file'] != false &&
bind.mainHasFileClipboard() && (isSupportIfPeer_1_2_3 || isSupportIfPeer_1_2_4)) {
pi.platformAdditions.containsKey(kPlatformAdditionsHasFileClipboard)) {
final enabled = !ffiModel.viewOnly; final enabled = !ffiModel.viewOnly;
final option = 'enable-file-transfer'; final value = bind.sessionGetToggleOptionSync(
final value = sessionId: sessionId, arg: kOptionEnableFileCopyPaste);
bind.sessionGetToggleOptionSync(sessionId: sessionId, arg: option);
v.add(TToggleMenu( v.add(TToggleMenu(
value: value, value: value,
onChanged: enabled onChanged: enabled
? (value) { ? (value) {
if (value == null) return; if (value == null) return;
bind.sessionToggleOption(sessionId: sessionId, value: option); bind.sessionToggleOption(
sessionId: sessionId, value: kOptionEnableFileCopyPaste);
} }
: null, : null,
child: Text(translate('Enable file copy and paste')))); child: Text(translate('Enable file copy and paste'))));
@@ -478,7 +579,7 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
child: Text(translate('Disable clipboard')))); child: Text(translate('Disable clipboard'))));
} }
// lock after session end // lock after session end
if (ffiModel.keyboard) { if (ffiModel.keyboard && !ffiModel.isPeerAndroid) {
final enabled = !ffiModel.viewOnly; final enabled = !ffiModel.viewOnly;
final option = 'lock-after-session-end'; final option = 'lock-after-session-end';
final value = final value =
@@ -494,8 +595,7 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
child: Text(translate('Lock after session end')))); child: Text(translate('Lock after session end'))));
} }
if (useTextureRender && if (pi.isSupportMultiDisplay &&
pi.isSupportMultiDisplay &&
PrivacyModeState.find(id).isEmpty && PrivacyModeState.find(id).isEmpty &&
pi.displaysCount.value > 1 && pi.displaysCount.value > 1 &&
bind.mainGetUserDefaultOption(key: kKeyShowMonitorsToolbar) == 'Y') { bind.mainGetUserDefaultOption(key: kKeyShowMonitorsToolbar) == 'Y') {
@@ -507,13 +607,13 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
onChanged: (value) { onChanged: (value) {
if (value == null) return; if (value == null) return;
bind.sessionSetDisplaysAsIndividualWindows( bind.sessionSetDisplaysAsIndividualWindows(
sessionId: sessionId, value: value ? 'Y' : ''); sessionId: sessionId, value: value ? 'Y' : 'N');
}, },
child: Text(translate('Show displays as individual windows')))); child: Text(translate('Show displays as individual windows'))));
} }
final screenList = await getScreenRectList(); final isMultiScreens = !isWeb && (await getScreenRectList()).length > 1;
if (useTextureRender && pi.isSupportMultiDisplay && screenList.length > 1) { if (pi.isSupportMultiDisplay && isMultiScreens) {
final value = bind.sessionGetUseAllMyDisplaysForTheRemoteSession( final value = bind.sessionGetUseAllMyDisplaysForTheRemoteSession(
sessionId: ffi.sessionId) == sessionId: ffi.sessionId) ==
'Y'; 'Y';
@@ -522,7 +622,7 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
onChanged: (value) { onChanged: (value) {
if (value == null) return; if (value == null) return;
bind.sessionSetUseAllMyDisplaysForTheRemoteSession( bind.sessionSetUseAllMyDisplaysForTheRemoteSession(
sessionId: sessionId, value: value ? 'Y' : ''); sessionId: sessionId, value: value ? 'Y' : 'N');
}, },
child: Text(translate('Use all my displays for the remote session')))); child: Text(translate('Use all my displays for the remote session'))));
} }
@@ -548,6 +648,18 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
v.addAll(toolbarKeyboardToggles(ffi)); v.addAll(toolbarKeyboardToggles(ffi));
} }
// view mode (mobile only, desktop is in keyboard menu)
if (isMobile && versionCmp(pi.version, '1.2.0') >= 0) {
v.add(TToggleMenu(
value: ffiModel.viewOnly,
onChanged: (value) async {
if (value == null) return;
await bind.sessionToggleOption(
sessionId: ffi.sessionId, value: kOptionToggleViewOnly);
ffiModel.setViewOnly(id, value);
},
child: Text(translate('View Mode'))));
}
return v; return v;
} }
@@ -633,8 +745,8 @@ List<TToggleMenu> toolbarKeyboardToggles(FFI ffi) {
// swap key // swap key
if (ffiModel.keyboard && if (ffiModel.keyboard &&
((Platform.isMacOS && pi.platform != kPeerPlatformMacOS) || ((isMacOS && pi.platform != kPeerPlatformMacOS) ||
(!Platform.isMacOS && pi.platform == kPeerPlatformMacOS))) { (!isMacOS && pi.platform == kPeerPlatformMacOS))) {
final option = 'allow_swap_key'; final option = 'allow_swap_key';
final value = final value =
bind.sessionGetToggleOptionSync(sessionId: sessionId, arg: option); bind.sessionGetToggleOptionSync(sessionId: sessionId, arg: option);
@@ -655,7 +767,7 @@ List<TToggleMenu> toolbarKeyboardToggles(FFI ffi) {
var optionValue = var optionValue =
bind.sessionGetReverseMouseWheelSync(sessionId: sessionId) ?? ''; bind.sessionGetReverseMouseWheelSync(sessionId: sessionId) ?? '';
if (optionValue == '') { if (optionValue == '') {
optionValue = bind.mainGetUserDefaultOption(key: 'reverse_mouse_wheel'); optionValue = bind.mainGetUserDefaultOption(key: kKeyReverseMouseWheel);
} }
onChanged(bool? value) async { onChanged(bool? value) async {
if (value == null) return; if (value == null) return;
@@ -688,3 +800,106 @@ List<TToggleMenu> toolbarKeyboardToggles(FFI ffi) {
} }
return v; return v;
} }
bool showVirtualDisplayMenu(FFI ffi) {
if (ffi.ffiModel.pi.platform != kPeerPlatformWindows) {
return false;
}
if (!ffi.ffiModel.pi.isInstalled) {
return false;
}
if (ffi.ffiModel.pi.isRustDeskIdd || ffi.ffiModel.pi.isAmyuniIdd) {
return true;
}
return false;
}
List<Widget> getVirtualDisplayMenuChildren(
FFI ffi, String id, VoidCallback? clickCallBack) {
if (!showVirtualDisplayMenu(ffi)) {
return [];
}
final pi = ffi.ffiModel.pi;
final privacyModeState = PrivacyModeState.find(id);
if (pi.isRustDeskIdd) {
final virtualDisplays = ffi.ffiModel.pi.RustDeskVirtualDisplays;
final children = <Widget>[];
for (var i = 0; i < kMaxVirtualDisplayCount; i++) {
children.add(Obx(() => CkbMenuButton(
value: virtualDisplays.contains(i + 1),
onChanged: privacyModeState.isNotEmpty
? null
: (bool? value) async {
if (value != null) {
bind.sessionToggleVirtualDisplay(
sessionId: ffi.sessionId, index: i + 1, on: value);
clickCallBack?.call();
}
},
child: Text('${translate('Virtual display')} ${i + 1}'),
ffi: ffi,
)));
}
children.add(Divider());
children.add(Obx(() => MenuButton(
onPressed: privacyModeState.isNotEmpty
? null
: () {
bind.sessionToggleVirtualDisplay(
sessionId: ffi.sessionId,
index: kAllVirtualDisplay,
on: false);
clickCallBack?.call();
},
ffi: ffi,
child: Text(translate('Plug out all')),
)));
return children;
}
if (pi.isAmyuniIdd) {
final count = ffi.ffiModel.pi.amyuniVirtualDisplayCount;
final children = <Widget>[
Obx(() => Row(
children: [
TextButton(
onPressed: privacyModeState.isNotEmpty || count == 0
? null
: () {
bind.sessionToggleVirtualDisplay(
sessionId: ffi.sessionId, index: 0, on: false);
clickCallBack?.call();
},
child: Icon(Icons.remove),
),
Text(count.toString()),
TextButton(
onPressed: privacyModeState.isNotEmpty || count == 4
? null
: () {
bind.sessionToggleVirtualDisplay(
sessionId: ffi.sessionId, index: 0, on: true);
clickCallBack?.call();
},
child: Icon(Icons.add),
),
],
)),
Divider(),
Obx(() => MenuButton(
onPressed: privacyModeState.isNotEmpty || count == 0
? null
: () {
bind.sessionToggleVirtualDisplay(
sessionId: ffi.sessionId,
index: kAllVirtualDisplay,
on: false);
clickCallBack?.call();
},
ffi: ffi,
child: Text(translate('Plug out all')),
)),
];
return children;
}
return [];
}

View File

@@ -1,5 +1,3 @@
import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/models/state_model.dart'; import 'package:flutter_hbb/models/state_model.dart';
@@ -21,14 +19,20 @@ const kKeyTranslateMode = 'translate';
const String kPlatformAdditionsIsWayland = "is_wayland"; const String kPlatformAdditionsIsWayland = "is_wayland";
const String kPlatformAdditionsHeadless = "headless"; const String kPlatformAdditionsHeadless = "headless";
const String kPlatformAdditionsIsInstalled = "is_installed"; const String kPlatformAdditionsIsInstalled = "is_installed";
const String kPlatformAdditionsVirtualDisplays = "virtual_displays"; const String kPlatformAdditionsIddImpl = "idd_impl";
const String kPlatformAdditionsRustDeskVirtualDisplays =
"rustdesk_virtual_displays";
const String kPlatformAdditionsAmyuniVirtualDisplays =
"amyuni_virtual_displays";
const String kPlatformAdditionsHasFileClipboard = "has_file_clipboard"; const String kPlatformAdditionsHasFileClipboard = "has_file_clipboard";
const String kPlatformAdditionsSupportedPrivacyModeImpl = "supported_privacy_mode_impl"; const String kPlatformAdditionsSupportedPrivacyModeImpl =
"supported_privacy_mode_impl";
const String kPeerPlatformWindows = "Windows"; const String kPeerPlatformWindows = "Windows";
const String kPeerPlatformLinux = "Linux"; const String kPeerPlatformLinux = "Linux";
const String kPeerPlatformMacOS = "Mac OS"; const String kPeerPlatformMacOS = "Mac OS";
const String kPeerPlatformAndroid = "Android"; const String kPeerPlatformAndroid = "Android";
const String kPeerPlatformWebDesktop = "WebDesktop";
const double kScrollbarThickness = 12.0; const double kScrollbarThickness = 12.0;
@@ -59,17 +63,100 @@ const String kWindowEventActiveSession = "active_session";
const String kWindowEventActiveDisplaySession = "active_display_session"; const String kWindowEventActiveDisplaySession = "active_display_session";
const String kWindowEventGetRemoteList = "get_remote_list"; const String kWindowEventGetRemoteList = "get_remote_list";
const String kWindowEventGetSessionIdList = "get_session_id_list"; const String kWindowEventGetSessionIdList = "get_session_id_list";
const String kWindowEventRemoteWindowCoords = "remote_window_coords";
const String kWindowEventSetFullscreen = "set_fullscreen";
const String kWindowEventMoveTabToNewWindow = "move_tab_to_new_window"; const String kWindowEventMoveTabToNewWindow = "move_tab_to_new_window";
const String kWindowEventGetCachedSessionData = "get_cached_session_data"; const String kWindowEventGetCachedSessionData = "get_cached_session_data";
const String kWindowEventOpenMonitorSession = "open_monitor_session"; const String kWindowEventOpenMonitorSession = "open_monitor_session";
const String kOptionViewStyle = "view_style";
const String kOptionScrollStyle = "scroll_style";
const String kOptionImageQuality = "image_quality";
const String kOptionOpenNewConnInTabs = "enable-open-new-connections-in-tabs"; const String kOptionOpenNewConnInTabs = "enable-open-new-connections-in-tabs";
const String kOptionTextureRender = "use-texture-render";
const String kOptionOpenInTabs = "allow-open-in-tabs"; const String kOptionOpenInTabs = "allow-open-in-tabs";
const String kOptionOpenInWindows = "allow-open-in-windows"; const String kOptionOpenInWindows = "allow-open-in-windows";
const String kOptionForceAlwaysRelay = "force-always-relay"; const String kOptionForceAlwaysRelay = "force-always-relay";
const String kOptionViewOnly = "view_only";
const String kOptionEnableLanDiscovery = "enable-lan-discovery";
const String kOptionWhitelist = "whitelist";
const String kOptionEnableAbr = "enable-abr";
const String kOptionEnableRecordSession = "enable-record-session";
const String kOptionDirectServer = "direct-server";
const String kOptionDirectAccessPort = "direct-access-port";
const String kOptionAllowAutoDisconnect = "allow-auto-disconnect";
const String kOptionAutoDisconnectTimeout = "auto-disconnect-timeout";
const String kOptionEnableHwcodec = "enable-hwcodec";
const String kOptionAllowAutoRecordIncoming = "allow-auto-record-incoming";
const String kOptionVideoSaveDirectory = "video-save-directory";
const String kOptionAccessMode = "access-mode";
const String kOptionEnableKeyboard = "enable-keyboard";
// "Settings -> Security -> Permissions"
const String kOptionEnableClipboard = "enable-clipboard";
const String kOptionEnableFileTransfer = "enable-file-transfer";
const String kOptionEnableAudio = "enable-audio";
const String kOptionEnableTunnel = "enable-tunnel";
const String kOptionEnableRemoteRestart = "enable-remote-restart";
const String kOptionEnableBlockInput = "enable-block-input";
const String kOptionAllowRemoteConfigModification =
"allow-remote-config-modification";
const String kOptionVerificationMethod = "verification-method";
const String kOptionApproveMode = "approve-mode";
const String kOptionCollapseToolbar = "collapse_toolbar";
const String kOptionShowRemoteCursor = "show_remote_cursor";
const String kOptionFollowRemoteCursor = "follow_remote_cursor";
const String kOptionFollowRemoteWindow = "follow_remote_window";
const String kOptionZoomCursor = "zoom-cursor";
const String kOptionShowQualityMonitor = "show_quality_monitor";
const String kOptionDisableAudio = "disable_audio";
const String kOptionEnableFileCopyPaste = "enable-file-copy-paste";
// "Settings -> Display -> Other default options"
const String kOptionDisableClipboard = "disable_clipboard";
const String kOptionLockAfterSessionEnd = "lock_after_session_end";
const String kOptionPrivacyMode = "privacy_mode";
const String kOptionTouchMode = "touch-mode";
const String kOptionI444 = "i444";
const String kOptionSwapLeftRightMouse = "swap-left-right-mouse";
const String kOptionCodecPreference = "codec-preference";
const String kOptionRemoteMenubarDragLeft = "remote-menubar-drag-left";
const String kOptionRemoteMenubarDragRight = "remote-menubar-drag-right";
const String kOptionHideAbTagsPanel = "hideAbTagsPanel";
const String kOptionRemoteMenubarState = "remoteMenubarState";
const String kOptionPeerSorting = "peer-sorting";
const String kOptionPeerTabIndex = "peer-tab-index";
const String kOptionPeerTabOrder = "peer-tab-order";
const String kOptionPeerTabVisible = "peer-tab-visible";
const String kOptionPeerCardUiType = "peer-card-ui-type";
const String kOptionCurrentAbName = "current-ab-name";
const String kOptionEnableConfirmClosingTabs = "enable-confirm-closing-tabs";
const String kOptionAllowAlwaysSoftwareRender = "allow-always-software-render";
const String kOptionEnableCheckUpdate = "enable-check-update";
const String kOptionAllowLinuxHeadless = "allow-linux-headless";
const String kOptionAllowRemoveWallpaper = "allow-remove-wallpaper";
const String kOptionStopService = "stop-service";
const String kOptionDirectxCapture = "enable-directx-capture";
const String kOptionAllowRemoteCmModification = "allow-remote-cm-modification";
const String kOptionEnableTrustedDevices = "enable-trusted-devices";
// buildin opitons
const String kOptionHideServerSetting = "hide-server-settings";
const String kOptionHideProxySetting = "hide-proxy-settings";
const String kOptionHideSecuritySetting = "hide-security-settings";
const String kOptionHideNetworkSetting = "hide-network-settings";
const String kOptionRemovePresetPasswordWarning =
"remove-preset-password-warning";
const kHideUsernameOnCard = "hide-username-on-card";
const String kOptionHideHelpCards = "hide-help-cards";
const String kOptionToggleViewOnly = "view-only";
const String kOptionDisableFloatingWindow = "disable-floating-window";
const String kOptionKeepScreenOn = "keep-screen-on";
const String kOptionShowMobileAction = "showMobileActions";
const String kUniLinksPrefix = "rustdesk://";
const String kUrlActionClose = "close"; const String kUrlActionClose = "close";
const String kTabLabelHomePage = "Home"; const String kTabLabelHomePage = "Home";
@@ -81,16 +168,22 @@ const int kWindowMainId = 0;
const String kPointerEventKindTouch = "touch"; const String kPointerEventKindTouch = "touch";
const String kPointerEventKindMouse = "mouse"; const String kPointerEventKindMouse = "mouse";
const String kKeyFlutterKey = "flutter_key";
const String kKeyShowDisplaysAsIndividualWindows = const String kKeyShowDisplaysAsIndividualWindows =
'displays_as_individual_windows'; 'displays_as_individual_windows';
const String kKeyUseAllMyDisplaysForTheRemoteSession = const String kKeyUseAllMyDisplaysForTheRemoteSession =
'use_all_my_displays_for_the_remote_session'; 'use_all_my_displays_for_the_remote_session';
const String kKeyShowMonitorsToolbar = 'show_monitors_toolbar'; const String kKeyShowMonitorsToolbar = 'show_monitors_toolbar';
const String kKeyReverseMouseWheel = "reverse_mouse_wheel";
const String kMsgboxTextWaitingForImage = 'Connected, waiting for image...';
// the executable name of the portable version // the executable name of the portable version
const String kEnvPortableExecutable = "RUSTDESK_APPNAME"; const String kEnvPortableExecutable = "RUSTDESK_APPNAME";
const Color kColorWarn = Color.fromARGB(255, 245, 133, 59); const Color kColorWarn = Color.fromARGB(255, 245, 133, 59);
const Color kColorCanvas = Colors.black;
const int kMobileDefaultDisplayWidth = 720; const int kMobileDefaultDisplayWidth = 720;
const int kMobileDefaultDisplayHeight = 1280; const int kMobileDefaultDisplayHeight = 1280;
@@ -108,25 +201,24 @@ const double kMinFps = 5;
const double kDefaultFps = 30; const double kDefaultFps = 30;
const double kMaxFps = 120; const double kMaxFps = 120;
const double kMinQuality = 10; const double kMinQuality = 5;
const double kDefaultQuality = 50; const double kDefaultQuality = 50;
const double kMaxQuality = 100; const double kMaxQuality = 100;
const double kMaxMoreQuality = 2000; const double kMaxMoreQuality = 2000;
double kNewWindowOffset = Platform.isWindows double kNewWindowOffset = isWindows
? 56.0 ? 56.0
: Platform.isLinux : isLinux
? 50.0 ? 50.0
: Platform.isMacOS : isMacOS
? 30.0 ? 30.0
: 50.0; : 50.0;
EdgeInsets get kDragToResizeAreaPadding => EdgeInsets get kDragToResizeAreaPadding => !kUseCompatibleUiMode && isLinux
!kUseCompatibleUiMode && Platform.isLinux ? stateGlobal.fullscreen.isTrue || stateGlobal.isMaximized.value
? stateGlobal.fullscreen.isTrue || stateGlobal.isMaximized.value ? EdgeInsets.zero
? EdgeInsets.zero : EdgeInsets.all(5.0)
: EdgeInsets.all(5.0) : 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;
@@ -150,9 +242,15 @@ const kDefaultScrollDuration = Duration(milliseconds: 50);
const kDefaultMouseWheelThrottleDuration = Duration(milliseconds: 50); const kDefaultMouseWheelThrottleDuration = Duration(milliseconds: 50);
const kFullScreenEdgeSize = 0.0; const kFullScreenEdgeSize = 0.0;
const kMaximizeEdgeSize = 0.0; const kMaximizeEdgeSize = 0.0;
var kWindowEdgeSize = Platform.isWindows ? 1.0 : 5.0; // Do not use kWindowResizeEdgeSize directly. Use `windowResizeEdgeSize` in `common.dart` instead.
const kWindowResizeEdgeSize = 5.0;
const kWindowBorderWidth = 1.0; const kWindowBorderWidth = 1.0;
const kDesktopMenuPadding = EdgeInsets.only(left: 12.0, right: 3.0); const kDesktopMenuPadding = EdgeInsets.only(left: 12.0, right: 3.0);
const kFrameBorderRadius = 12.0;
const kFrameClipRRectBorderRadius = 12.0;
const kFrameBoxShadowBlurRadius = 32.0;
const kFrameBoxShadowOffsetFocused = 4.0;
const kFrameBoxShadowOffsetUnfocused = 2.0;
const kInvalidValueStr = 'InvalidValueStr'; const kInvalidValueStr = 'InvalidValueStr';
@@ -198,12 +296,6 @@ const kRemoteImageQualityLow = 'low';
/// [kRemoteImageQualityCustom] Custom image quality. /// [kRemoteImageQualityCustom] Custom image quality.
const kRemoteImageQualityCustom = 'custom'; const kRemoteImageQualityCustom = 'custom';
/// [kRemoteAudioGuestToHost] Guest to host audio mode(default).
const kRemoteAudioGuestToHost = 'guest-to-host';
/// [kRemoteAudioDualWay] dual-way audio mode(default).
const kRemoteAudioDualWay = 'dual-way';
const kIgnoreDpi = true; const kIgnoreDpi = true;
// ================================ mobile ================================ // ================================ mobile ================================
@@ -222,6 +314,7 @@ 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";
const kAndroid13Notification = "android.permission.POST_NOTIFICATIONS";
/// Android channel invoke type key /// Android channel invoke type key
class AndroidChannel { class AndroidChannel {
@@ -477,3 +570,5 @@ enum WindowsTarget {
extension WindowsTargetExt on int { extension WindowsTargetExt on int {
WindowsTarget get windowsVersion => getWindowsTarget(this); WindowsTarget get windowsVersion => getWindowsTarget(this);
} }
const kCheckSoftwareUpdateFinish = 'check_software_update_finish';

View File

@@ -2,10 +2,9 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:io';
import 'package:auto_size_text/auto_size_text.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hbb/common/widgets/connection_page_title.dart';
import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/models/state_model.dart'; import 'package:flutter_hbb/models/state_model.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
@@ -20,6 +19,169 @@ import '../../common/widgets/autocomplete.dart';
import '../../models/platform_model.dart'; import '../../models/platform_model.dart';
import '../widgets/button.dart'; import '../widgets/button.dart';
class OnlineStatusWidget extends StatefulWidget {
const OnlineStatusWidget({Key? key, this.onSvcStatusChanged})
: super(key: key);
final VoidCallback? onSvcStatusChanged;
@override
State<OnlineStatusWidget> createState() => _OnlineStatusWidgetState();
}
/// State for the connection page.
class _OnlineStatusWidgetState extends State<OnlineStatusWidget> {
final _svcStopped = Get.find<RxBool>(tag: 'stop-service');
final _svcIsUsingPublicServer = true.obs;
Timer? _updateTimer;
double get em => 14.0;
double? get height => bind.isIncomingOnly() ? null : em * 3;
void onUsePublicServerGuide() {
const url = "https://rustdesk.com/pricing.html";
canLaunchUrlString(url).then((can) {
if (can) {
launchUrlString(url);
}
});
}
@override
void initState() {
super.initState();
_updateTimer = periodic_immediate(Duration(seconds: 1), () async {
updateStatus();
});
}
@override
void dispose() {
_updateTimer?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
final isIncomingOnly = bind.isIncomingOnly();
startServiceWidget() => Offstage(
offstage: !_svcStopped.value,
child: InkWell(
onTap: () async {
await start_service(true);
},
child: Text(translate("Start service"),
style: TextStyle(
decoration: TextDecoration.underline, fontSize: em)))
.marginOnly(left: em),
);
setupServerWidget() => Flexible(
child: Offstage(
offstage: !(!_svcStopped.value &&
stateGlobal.svcStatus.value == SvcStatus.ready &&
_svcIsUsingPublicServer.value),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(', ', style: TextStyle(fontSize: em)),
Flexible(
child: InkWell(
onTap: onUsePublicServerGuide,
child: Row(
children: [
Flexible(
child: Text(
translate('setup_server_tip'),
style: TextStyle(
decoration: TextDecoration.underline,
fontSize: em),
),
),
],
),
),
)
],
),
),
);
basicWidget() => Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Container(
height: 8,
width: 8,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4),
color: _svcStopped.value ||
stateGlobal.svcStatus.value == SvcStatus.connecting
? kColorWarn
: (stateGlobal.svcStatus.value == SvcStatus.ready
? Color.fromARGB(255, 50, 190, 166)
: Color.fromARGB(255, 224, 79, 95)),
),
).marginSymmetric(horizontal: em),
Container(
width: isIncomingOnly ? 226 : null,
child: _buildConnStatusMsg(),
),
// stop
if (!isIncomingOnly) startServiceWidget(),
// ready && public
// No need to show the guide if is custom client.
if (!isIncomingOnly) setupServerWidget(),
],
);
return Container(
height: height,
child: Obx(() => isIncomingOnly
? Column(
children: [
basicWidget(),
Align(
child: startServiceWidget(),
alignment: Alignment.centerLeft)
.marginOnly(top: 2.0, left: 22.0),
],
)
: basicWidget()),
).paddingOnly(right: isIncomingOnly ? 8 : 0);
}
_buildConnStatusMsg() {
widget.onSvcStatusChanged?.call();
return Text(
_svcStopped.value
? translate("Service is not running")
: stateGlobal.svcStatus.value == SvcStatus.connecting
? translate("connecting_status")
: stateGlobal.svcStatus.value == SvcStatus.notReady
? translate("not_ready_status")
: translate('Ready'),
style: TextStyle(fontSize: em),
);
}
updateStatus() async {
final status =
jsonDecode(await bind.mainGetConnectStatus()) as Map<String, dynamic>;
final statusNum = status['status_num'] as int;
if (statusNum == 0) {
stateGlobal.svcStatus.value = SvcStatus.connecting;
} else if (statusNum == -1) {
stateGlobal.svcStatus.value = SvcStatus.notReady;
} else if (statusNum == 1) {
stateGlobal.svcStatus.value = SvcStatus.ready;
} else {
stateGlobal.svcStatus.value = SvcStatus.notReady;
}
_svcIsUsingPublicServer.value = await bind.mainIsUsingPublicServer();
}
}
/// Connection page for connecting to a remote peer. /// Connection page for connecting to a remote peer.
class ConnectionPage extends StatefulWidget { class ConnectionPage extends StatefulWidget {
const ConnectionPage({Key? key}) : super(key: key); const ConnectionPage({Key? key}) : super(key: key);
@@ -34,13 +196,8 @@ class _ConnectionPageState extends State<ConnectionPage>
/// Controller for the id input bar. /// Controller for the id input bar.
final _idController = IDTextEditingController(); final _idController = IDTextEditingController();
Timer? _updateTimer;
final RxBool _idInputFocused = false.obs; final RxBool _idInputFocused = false.obs;
var svcStopped = Get.find<RxBool>(tag: 'stop-service');
var svcIsUsingPublicServer = true.obs;
bool isWindowMinimized = false; bool isWindowMinimized = false;
List<Peer> peers = []; List<Peer> peers = [];
@@ -51,18 +208,15 @@ class _ConnectionPageState extends State<ConnectionPage>
void initState() { void initState() {
super.initState(); super.initState();
if (_idController.text.isEmpty) { if (_idController.text.isEmpty) {
() async { WidgetsBinding.instance.addPostFrameCallback((_) async {
final lastRemoteId = await bind.mainGetLastRemoteId(); final lastRemoteId = await bind.mainGetLastRemoteId();
if (lastRemoteId != _idController.id) { if (lastRemoteId != _idController.id) {
setState(() { setState(() {
_idController.id = lastRemoteId; _idController.id = lastRemoteId;
}); });
} }
}(); });
} }
_updateTimer = periodic_immediate(Duration(seconds: 1), () async {
updateStatus();
});
Get.put<IDTextEditingController>(_idController); Get.put<IDTextEditingController>(_idController);
windowManager.addListener(this); windowManager.addListener(this);
} }
@@ -70,7 +224,6 @@ class _ConnectionPageState extends State<ConnectionPage>
@override @override
void dispose() { void dispose() {
_idController.dispose(); _idController.dispose();
_updateTimer?.cancel();
windowManager.removeListener(this); windowManager.removeListener(this);
if (Get.isRegistered<IDTextEditingController>()) { if (Get.isRegistered<IDTextEditingController>()) {
Get.delete<IDTextEditingController>(); Get.delete<IDTextEditingController>();
@@ -87,7 +240,7 @@ class _ConnectionPageState extends State<ConnectionPage>
if (eventName == 'minimize') { if (eventName == 'minimize') {
isWindowMinimized = true; isWindowMinimized = true;
} else if (eventName == 'maximize' || eventName == 'restore') { } else if (eventName == 'maximize' || eventName == 'restore') {
if (isWindowMinimized && Platform.isWindows) { if (isWindowMinimized && isWindows) {
// windows can't update when minimized. // windows can't update when minimized.
Get.forceAppUpdate(); Get.forceAppUpdate();
} }
@@ -104,8 +257,9 @@ class _ConnectionPageState extends State<ConnectionPage>
@override @override
void onWindowLeaveFullScreen() { void onWindowLeaveFullScreen() {
// Restore edge border to default edge size. // Restore edge border to default edge size.
stateGlobal.resizeEdgeSize.value = stateGlobal.resizeEdgeSize.value = stateGlobal.isMaximized.isTrue
stateGlobal.isMaximized.isTrue ? kMaximizeEdgeSize : kWindowEdgeSize; ? kMaximizeEdgeSize
: windowResizeEdgeSize;
} }
@override @override
@@ -116,6 +270,7 @@ class _ConnectionPageState extends State<ConnectionPage>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final isOutgoingOnly = bind.isOutgoingOnly();
return Column( return Column(
children: [ children: [
Expanded( Expanded(
@@ -131,8 +286,8 @@ class _ConnectionPageState extends State<ConnectionPage>
Expanded(child: PeerTabPage()), Expanded(child: PeerTabPage()),
], ],
).paddingOnly(left: 12.0)), ).paddingOnly(left: 12.0)),
const Divider(height: 1), if (!isOutgoingOnly) const Divider(height: 1),
buildStatus() if (!isOutgoingOnly) OnlineStatusWidget()
], ],
); );
} }
@@ -168,36 +323,7 @@ class _ConnectionPageState extends State<ConnectionPage>
child: Ink( child: Ink(
child: Column( child: Column(
children: [ children: [
Row( getConnectionPageTitle(context, false).marginOnly(bottom: 15),
children: [
Expanded(
child: Row(
children: [
AutoSizeText(
translate('Control Remote Desktop'),
maxLines: 1,
style: Theme.of(context)
.textTheme
.titleLarge
?.merge(TextStyle(height: 1)),
).marginOnly(right: 4),
Tooltip(
waitDuration: Duration(milliseconds: 0),
message: translate("id_input_tip"),
child: Icon(
Icons.help_outline_outlined,
size: 16,
color: Theme.of(context)
.textTheme
.titleLarge
?.color
?.withOpacity(0.5),
),
),
],
)),
],
).marginOnly(bottom: 15),
Row( Row(
children: [ children: [
Expanded( Expanded(
@@ -214,6 +340,7 @@ class _ConnectionPageState extends State<ConnectionPage>
platform: '', platform: '',
tags: [], tags: [],
hash: '', hash: '',
password: '',
forceAlwaysRelay: false, forceAlwaysRelay: false,
rdpPort: '', rdpPort: '',
rdpUsername: '', rdpUsername: '',
@@ -289,6 +416,9 @@ class _ConnectionPageState extends State<ConnectionPage>
onChanged: (v) { onChanged: (v) {
_idController.id = v; _idController.id = v;
}, },
onSubmitted: (_) {
onConnect();
},
)); ));
}, },
onSelected: (option) { onSelected: (option) {
@@ -383,111 +513,4 @@ class _ConnectionPageState extends State<ConnectionPage>
return Container( return Container(
constraints: const BoxConstraints(maxWidth: 600), child: w); constraints: const BoxConstraints(maxWidth: 600), child: w);
} }
Widget buildStatus() {
final em = 14.0;
return Container(
height: 3 * em,
child: Obx(() => Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Container(
height: 8,
width: 8,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4),
color: svcStopped.value ||
stateGlobal.svcStatus.value == SvcStatus.connecting
? kColorWarn
: (stateGlobal.svcStatus.value == SvcStatus.ready
? Color.fromARGB(255, 50, 190, 166)
: Color.fromARGB(255, 224, 79, 95)),
),
).marginSymmetric(horizontal: em),
Text(
svcStopped.value
? translate("Service is not running")
: stateGlobal.svcStatus.value == SvcStatus.connecting
? translate("connecting_status")
: stateGlobal.svcStatus.value == SvcStatus.notReady
? translate("not_ready_status")
: translate('Ready'),
style: TextStyle(fontSize: em)),
// stop
Offstage(
offstage: !svcStopped.value,
child: InkWell(
onTap: () async {
await start_service(true);
},
child: Text(translate("Start service"),
style: TextStyle(
decoration: TextDecoration.underline,
fontSize: em)))
.marginOnly(left: em),
),
// ready && public
Flexible(
child: Offstage(
offstage: !(!svcStopped.value &&
stateGlobal.svcStatus.value == SvcStatus.ready &&
svcIsUsingPublicServer.value),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(', ', style: TextStyle(fontSize: em)),
Flexible(
child: InkWell(
onTap: onUsePublicServerGuide,
child: Row(
children: [
Flexible(
child: Text(
translate('setup_server_tip'),
style: TextStyle(
decoration: TextDecoration.underline,
fontSize: em),
),
),
],
),
),
)
],
),
),
)
],
)),
);
}
void onUsePublicServerGuide() {
const url = "https://rustdesk.com/pricing.html";
canLaunchUrlString(url).then((can) {
if (can) {
launchUrlString(url);
}
});
}
updateStatus() async {
final status =
jsonDecode(await bind.mainGetConnectStatus()) as Map<String, dynamic>;
final statusNum = status['status_num'] as int;
final preStatus = stateGlobal.svcStatus.value;
if (statusNum == 0) {
stateGlobal.svcStatus.value = SvcStatus.connecting;
} else if (statusNum == -1) {
stateGlobal.svcStatus.value = SvcStatus.notReady;
} else if (statusNum == 1) {
stateGlobal.svcStatus.value = SvcStatus.ready;
if (preStatus != SvcStatus.ready) {
gFFI.userModel.refreshCurrentUser();
}
} else {
stateGlobal.svcStatus.value = SvcStatus.notReady;
}
svcIsUsingPublicServer.value = await bind.mainIsUsingPublicServer();
}
} }

View File

@@ -20,6 +20,7 @@ import 'package:flutter_hbb/utils/multi_window_manager.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';
import 'package:window_manager/window_manager.dart';
import 'package:window_size/window_size.dart' as window_size; import 'package:window_size/window_size.dart' as window_size;
import '../widgets/button.dart'; import '../widgets/button.dart';
@@ -50,50 +51,120 @@ class _DesktopHomePageState extends State<DesktopHomePage>
Timer? _updateTimer; Timer? _updateTimer;
bool isCardClosed = false; bool isCardClosed = false;
final RxBool _editHover = false.obs;
final GlobalKey _childKey = GlobalKey();
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
super.build(context); super.build(context);
final isIncomingOnly = bind.isIncomingOnly();
return Row( return Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
buildLeftPane(context), buildLeftPane(context),
const VerticalDivider(width: 1), if (!isIncomingOnly) const VerticalDivider(width: 1),
Expanded( if (!isIncomingOnly) Expanded(child: buildRightPane(context)),
child: buildRightPane(context),
),
], ],
); );
} }
Widget buildLeftPane(BuildContext context) { Widget buildLeftPane(BuildContext context) {
final isIncomingOnly = bind.isIncomingOnly();
final isOutgoingOnly = bind.isOutgoingOnly();
final children = <Widget>[
if (!isOutgoingOnly) buildPresetPasswordWarning(),
if (bind.isCustomClient())
Align(
alignment: Alignment.center,
child: loadPowered(context),
),
Align(
alignment: Alignment.center,
child: loadLogo(),
),
buildTip(context),
if (!isOutgoingOnly) buildIDBoard(context),
if (!isOutgoingOnly) buildPasswordBoard(context),
FutureBuilder<Widget>(
future: buildHelpCards(),
builder: (_, data) {
if (data.hasData) {
if (isIncomingOnly) {
if (isInHomePage()) {
Future.delayed(Duration(milliseconds: 300), () {
_updateWindowSize();
});
}
}
return data.data!;
} else {
return const Offstage();
}
},
),
buildPluginEntry(),
];
if (isIncomingOnly) {
children.addAll([
Divider(),
OnlineStatusWidget(
onSvcStatusChanged: () {
if (isInHomePage()) {
Future.delayed(Duration(milliseconds: 300), () {
_updateWindowSize();
});
}
},
).marginOnly(bottom: 6, right: 6)
]);
}
final textColor = Theme.of(context).textTheme.titleLarge?.color;
return ChangeNotifierProvider.value( return ChangeNotifierProvider.value(
value: gFFI.serverModel, value: gFFI.serverModel,
child: Container( child: Container(
width: 200, width: isIncomingOnly ? 280.0 : 200.0,
color: Theme.of(context).colorScheme.background, color: Theme.of(context).colorScheme.background,
child: DesktopScrollWrapper( child: DesktopScrollWrapper(
scrollController: _leftPaneScrollController, scrollController: _leftPaneScrollController,
child: SingleChildScrollView( child: Stack(
controller: _leftPaneScrollController, children: [
physics: DraggableNeverScrollableScrollPhysics(), SingleChildScrollView(
child: Column( controller: _leftPaneScrollController,
children: [ physics: DraggableNeverScrollableScrollPhysics(),
buildTip(context), child: Column(
buildIDBoard(context), key: _childKey,
buildPasswordBoard(context), children: children,
FutureBuilder<Widget>(
future: buildHelpCards(),
builder: (_, data) {
if (data.hasData) {
return data.data!;
} else {
return const Offstage();
}
},
), ),
buildPluginEntry() ),
], if (isOutgoingOnly)
), Positioned(
bottom: 6,
left: 12,
child: Align(
alignment: Alignment.centerLeft,
child: InkWell(
child: Obx(
() => Icon(
Icons.settings,
color: _editHover.value
? textColor
: Colors.grey.withOpacity(0.5),
size: 22,
),
),
onTap: () => {
if (DesktopSettingPage.tabKeys.isNotEmpty)
{
DesktopSettingPage.switch2page(
DesktopSettingPage.tabKeys[0])
}
},
onHover: (value) => _editHover.value = value,
),
),
)
],
), ),
), ),
), ),
@@ -180,19 +251,20 @@ class _DesktopHomePageState extends State<DesktopHomePage>
RxBool hover = false.obs; RxBool hover = false.obs;
return InkWell( return InkWell(
onTap: DesktopTabPage.onAddSetting, onTap: DesktopTabPage.onAddSetting,
child: Obx( child: Tooltip(
() => CircleAvatar( message: translate('Settings'),
radius: 15, child: Obx(
backgroundColor: hover.value () => CircleAvatar(
? Theme.of(context).scaffoldBackgroundColor radius: 15,
: Theme.of(context).colorScheme.background, backgroundColor: hover.value
child: Tooltip( ? Theme.of(context).scaffoldBackgroundColor
message: translate('Settings'), : Theme.of(context).colorScheme.background,
child: Icon( child: Icon(
Icons.more_vert_outlined, Icons.more_vert_outlined,
size: 20, size: 20,
color: hover.value ? textColor : textColor?.withOpacity(0.5), color: hover.value ? textColor : textColor?.withOpacity(0.5),
)), ),
),
), ),
), ),
onHover: (value) => hover.value = value, onHover: (value) => hover.value = value,
@@ -253,34 +325,38 @@ class _DesktopHomePageState extends State<DesktopHomePage>
), ),
AnimatedRotationWidget( AnimatedRotationWidget(
onPressed: () => bind.mainUpdateTemporaryPassword(), onPressed: () => bind.mainUpdateTemporaryPassword(),
child: Obx(() => RotatedBox( child: Tooltip(
quarterTurns: 2, message: translate('Refresh Password'),
child: Tooltip( child: Obx(() => RotatedBox(
message: translate('Refresh Password'), quarterTurns: 2,
child: Icon( child: Icon(
Icons.refresh, Icons.refresh,
color: refreshHover.value color: refreshHover.value
? textColor ? textColor
: Color(0xFFDDDDDD), : Color(0xFFDDDDDD),
size: 22, size: 22,
)))), ))),
),
onHover: (value) => refreshHover.value = value, onHover: (value) => refreshHover.value = value,
).marginOnly(right: 8, top: 4), ).marginOnly(right: 8, top: 4),
InkWell( if (!bind.isDisableSettings())
child: Obx( InkWell(
() => Tooltip( child: Tooltip(
message: translate('Change Password'), message: translate('Change Password'),
child: Icon( child: Obx(
() => Icon(
Icons.edit, Icons.edit,
color: editHover.value color: editHover.value
? textColor ? textColor
: Color(0xFFDDDDDD), : Color(0xFFDDDDDD),
size: 22, size: 22,
)).marginOnly(right: 8, top: 4), ).marginOnly(right: 8, top: 4),
),
),
onTap: () => DesktopSettingPage.switch2page(
SettingsTabKey.safety),
onHover: (value) => editHover.value = value,
), ),
onTap: () => DesktopSettingPage.switch2page(1),
onHover: (value) => editHover.value = value,
),
], ],
), ),
], ],
@@ -293,6 +369,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
} }
buildTip(BuildContext context) { buildTip(BuildContext context) {
final isOutgoingOnly = bind.isOutgoingOnly();
return Padding( return Padding(
padding: padding:
const EdgeInsets.only(left: 20.0, right: 16, top: 16.0, bottom: 5), const EdgeInsets.only(left: 20.0, right: 16, top: 16.0, bottom: 5),
@@ -300,29 +377,43 @@ class _DesktopHomePageState extends State<DesktopHomePage>
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Column(
translate("Your Desktop"), children: [
style: Theme.of(context).textTheme.titleLarge, if (!isOutgoingOnly)
// style: TextStyle( Align(
// // color: MyTheme.color(context).text, alignment: Alignment.centerLeft,
// fontWeight: FontWeight.normal, child: Text(
// fontSize: 19), translate("Your Desktop"),
style: Theme.of(context).textTheme.titleLarge,
),
),
],
), ),
SizedBox( SizedBox(
height: 10.0, height: 10.0,
), ),
Text( if (!isOutgoingOnly)
translate("desk_tip"), Text(
overflow: TextOverflow.clip, translate("desk_tip"),
style: Theme.of(context).textTheme.bodySmall, overflow: TextOverflow.clip,
) style: Theme.of(context).textTheme.bodySmall,
),
if (isOutgoingOnly)
Text(
translate("outgoing_only_desk_tip"),
overflow: TextOverflow.clip,
style: Theme.of(context).textTheme.bodySmall,
),
], ],
), ),
); );
} }
Future<Widget> buildHelpCards() async { Future<Widget> buildHelpCards() async {
if (updateUrl.isNotEmpty && !isCardClosed) { if (!bind.isCustomClient() &&
updateUrl.isNotEmpty &&
!isCardClosed &&
bind.mainUriPrefixSync().contains('rustdesk')) {
return buildInstallCard( return buildInstallCard(
"Status", "Status",
"There is a newer version of ${bind.mainGetAppNameSync()} ${bind.mainGetNewVersion()} available.", "There is a newer version of ${bind.mainGetAppNameSync()} ${bind.mainGetNewVersion()} available.",
@@ -334,9 +425,12 @@ class _DesktopHomePageState extends State<DesktopHomePage>
if (systemError.isNotEmpty) { if (systemError.isNotEmpty) {
return buildInstallCard("", systemError, "", () {}); return buildInstallCard("", systemError, "", () {});
} }
if (Platform.isWindows) {
if (isWindows && !bind.isDisableInstallation()) {
if (!bind.mainIsInstalled()) { if (!bind.mainIsInstalled()) {
return buildInstallCard("", "install_tip", "Install", () async { return buildInstallCard(
"", bind.isOutgoingOnly() ? "" : "install_tip", "Install",
() async {
await rustDeskWinManager.closeAllSubWindows(); await rustDeskWinManager.closeAllSubWindows();
bind.mainGotoInstall(); bind.mainGotoInstall();
}); });
@@ -348,14 +442,15 @@ class _DesktopHomePageState extends State<DesktopHomePage>
bind.mainUpdateMe(); bind.mainUpdateMe();
}); });
} }
} else if (Platform.isMacOS) { } else if (isMacOS) {
if (!bind.mainIsCanScreenRecording(prompt: false)) { final isOutgoingOnly = bind.isOutgoingOnly();
if (!(isOutgoingOnly || bind.mainIsCanScreenRecording(prompt: false))) {
return buildInstallCard("Permissions", "config_screen", "Configure", return buildInstallCard("Permissions", "config_screen", "Configure",
() async { () async {
bind.mainIsCanScreenRecording(prompt: true); bind.mainIsCanScreenRecording(prompt: true);
watchIsCanScreenRecording = true; watchIsCanScreenRecording = true;
}, help: 'Help', link: translate("doc_mac_permission")); }, help: 'Help', link: translate("doc_mac_permission"));
} else if (!bind.mainIsProcessTrusted(prompt: false)) { } else if (!isOutgoingOnly && !bind.mainIsProcessTrusted(prompt: false)) {
return buildInstallCard("Permissions", "config_acc", "Configure", return buildInstallCard("Permissions", "config_acc", "Configure",
() async { () async {
bind.mainIsProcessTrusted(prompt: true); bind.mainIsProcessTrusted(prompt: true);
@@ -367,7 +462,8 @@ class _DesktopHomePageState extends State<DesktopHomePage>
bind.mainIsCanInputMonitoring(prompt: true); bind.mainIsCanInputMonitoring(prompt: true);
watchIsInputMonitoring = true; watchIsInputMonitoring = true;
}, help: 'Help', link: translate("doc_mac_permission")); }, help: 'Help', link: translate("doc_mac_permission"));
} else if (!svcStopped.value && } else if (!isOutgoingOnly &&
!svcStopped.value &&
bind.mainIsInstalled() && bind.mainIsInstalled() &&
!bind.mainIsInstalledDaemon(prompt: false)) { !bind.mainIsInstalledDaemon(prompt: false)) {
return buildInstallCard("", "install_daemon_tip", "Install", () async { return buildInstallCard("", "install_daemon_tip", "Install", () async {
@@ -383,7 +479,10 @@ class _DesktopHomePageState extends State<DesktopHomePage>
// watchIsCanRecordAudio = true; // watchIsCanRecordAudio = true;
// }); // });
// } // }
} else if (Platform.isLinux) { } else if (isLinux) {
if (bind.isOutgoingOnly()) {
return Container();
}
final LinuxCards = <Widget>[]; final LinuxCards = <Widget>[];
if (bind.isSelinuxEnforcing()) { if (bind.isSelinuxEnforcing()) {
// Check is SELinux enforcing, but show user a tip of is SELinux enabled for simple. // Check is SELinux enforcing, but show user a tip of is SELinux enabled for simple.
@@ -422,6 +521,21 @@ class _DesktopHomePageState extends State<DesktopHomePage>
); );
} }
} }
if (bind.isIncomingOnly()) {
return Align(
alignment: Alignment.centerRight,
child: OutlinedButton(
onPressed: () {
SystemNavigator.pop(); // Close the application
// https://github.com/flutter/flutter/issues/66631
if (isWindows) {
exit(0);
}
},
child: Text(translate('Quit')),
),
).marginAll(14);
}
return Container(); return Container();
} }
@@ -432,6 +546,10 @@ class _DesktopHomePageState extends State<DesktopHomePage>
String? link, String? link,
bool? closeButton, bool? closeButton,
String? closeOption}) { String? closeOption}) {
if (bind.mainGetBuildinOption(key: kOptionHideHelpCards) == 'Y' &&
content != 'install_daemon_tip') {
return const SizedBox();
}
void closeCard() async { void closeCard() async {
if (closeOption != null) { if (closeOption != null) {
await bind.mainSetLocalOption(key: closeOption, value: 'N'); await bind.mainSetLocalOption(key: closeOption, value: 'N');
@@ -450,7 +568,8 @@ class _DesktopHomePageState extends State<DesktopHomePage>
return Stack( return Stack(
children: [ children: [
Container( Container(
margin: EdgeInsets.only(top: marginTop), margin: EdgeInsets.fromLTRB(
0, marginTop, 0, bind.isIncomingOnly() ? marginTop : 0),
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
gradient: LinearGradient( gradient: LinearGradient(
@@ -478,14 +597,15 @@ class _DesktopHomePageState extends State<DesktopHomePage>
] ]
: <Widget>[]) + : <Widget>[]) +
<Widget>[ <Widget>[
Text( if (content.isNotEmpty)
translate(content), Text(
style: TextStyle( translate(content),
height: 1.5, style: TextStyle(
color: Colors.white, height: 1.5,
fontWeight: FontWeight.normal, color: Colors.white,
fontSize: 13), fontWeight: FontWeight.normal,
).marginOnly(bottom: 20) fontSize: 13),
).marginOnly(bottom: 20)
] + ] +
(btnText.isNotEmpty (btnText.isNotEmpty
? <Widget>[ ? <Widget>[
@@ -543,10 +663,20 @@ class _DesktopHomePageState extends State<DesktopHomePage>
@override @override
void initState() { void initState() {
super.initState(); super.initState();
Timer(const Duration(seconds: 1), () async { if (!bind.isCustomClient()) {
updateUrl = await bind.mainGetSoftwareUpdateUrl(); platformFFI.registerEventHandler(
if (updateUrl.isNotEmpty) setState(() {}); kCheckSoftwareUpdateFinish, kCheckSoftwareUpdateFinish,
}); (Map<String, dynamic> evt) async {
if (evt['url'] is String) {
setState(() {
updateUrl = evt['url'];
});
}
});
Timer(const Duration(seconds: 1), () async {
bind.mainGetSoftwareUpdateUrl();
});
}
_updateTimer = periodic_immediate(const Duration(seconds: 1), () async { _updateTimer = periodic_immediate(const Duration(seconds: 1), () async {
await gFFI.serverModel.fetchID(); await gFFI.serverModel.fetchID();
final error = await bind.mainGetError(); final error = await bind.mainGetError();
@@ -554,7 +684,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
systemError = error; systemError = error;
setState(() {}); setState(() {});
} }
final v = await bind.mainGetOption(key: "stop-service") == "Y"; final v = await mainGetBoolOption(kOptionStopService);
if (v != svcStopped.value) { if (v != svcStopped.value) {
svcStopped.value = v; svcStopped.value = v;
setState(() {}); setState(() {});
@@ -582,7 +712,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
} }
} }
if (watchIsCanRecordAudio) { if (watchIsCanRecordAudio) {
if (Platform.isMacOS) { if (isMacOS) {
Future.microtask(() async { Future.microtask(() async {
if ((await osxCanRecordAudio() == if ((await osxCanRecordAudio() ==
PermissionAuthorizeType.authorized)) { PermissionAuthorizeType.authorized)) {
@@ -642,6 +772,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
isFileTransfer: call.arguments['isFileTransfer'], isFileTransfer: call.arguments['isFileTransfer'],
isTcpTunneling: call.arguments['isTcpTunneling'], isTcpTunneling: call.arguments['isTcpTunneling'],
isRDP: call.arguments['isRDP'], isRDP: call.arguments['isRDP'],
password: call.arguments['password'],
forceRelay: call.arguments['forceRelay'], forceRelay: call.arguments['forceRelay'],
); );
} else if (call.method == kWindowEventMoveTabToNewWindow) { } else if (call.method == kWindowEventMoveTabToNewWindow) {
@@ -665,9 +796,35 @@ class _DesktopHomePageState extends State<DesktopHomePage>
final screenRect = parseParamScreenRect(args); final screenRect = parseParamScreenRect(args);
await rustDeskWinManager.openMonitorSession( await rustDeskWinManager.openMonitorSession(
windowId, peerId, display, displayCount, screenRect); windowId, peerId, display, displayCount, screenRect);
} else if (call.method == kWindowEventRemoteWindowCoords) {
final windowId = int.tryParse(call.arguments);
if (windowId != null) {
return jsonEncode(
await rustDeskWinManager.getOtherRemoteWindowCoords(windowId));
}
} }
}); });
_uniLinksSubscription = listenUniLinks(); _uniLinksSubscription = listenUniLinks();
if (bind.isIncomingOnly()) {
WidgetsBinding.instance.addPostFrameCallback((_) {
_updateWindowSize();
});
}
}
_updateWindowSize() {
RenderObject? renderObject = _childKey.currentContext?.findRenderObject();
if (renderObject == null) {
return;
}
if (renderObject is RenderBox) {
final size = renderObject.size;
if (size != imcomingOnlyHomeSize) {
imcomingOnlyHomeSize = size;
windowManager.setSize(getIncomingOnlyHomeSize());
}
}
} }
@override @override
@@ -675,6 +832,10 @@ class _DesktopHomePageState extends State<DesktopHomePage>
_uniLinksSubscription?.cancel(); _uniLinksSubscription?.cancel();
Get.delete<RxBool>(tag: 'stop-service'); Get.delete<RxBool>(tag: 'stop-service');
_updateTimer?.cancel(); _updateTimer?.cancel();
if (!bind.isCustomClient()) {
platformFFI.unregisterEventHandler(
kCheckSoftwareUpdateFinish, kCheckSoftwareUpdateFinish);
}
super.dispose(); super.dispose();
} }
@@ -694,7 +855,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
} }
} }
void setPasswordDialog() async { void setPasswordDialog({VoidCallback? notEmptyCallback}) async {
final pw = await bind.mainGetPermanentPassword(); final pw = await bind.mainGetPermanentPassword();
final p0 = TextEditingController(text: pw); final p0 = TextEditingController(text: pw);
final p1 = TextEditingController(text: pw); final p1 = TextEditingController(text: pw);
@@ -708,6 +869,7 @@ void setPasswordDialog() async {
// SpecialCharacterValidationRule(), // SpecialCharacterValidationRule(),
MinCharactersValidationRule(8), MinCharactersValidationRule(8),
]; ];
final maxLength = bind.mainMaxEncryptLen();
gFFI.dialogManager.show((setState, close, context) { gFFI.dialogManager.show((setState, close, context) {
submit() { submit() {
@@ -734,6 +896,9 @@ void setPasswordDialog() async {
return; return;
} }
bind.mainSetPermanentPassword(password: pass); bind.mainSetPermanentPassword(password: pass);
if (pass.isNotEmpty) {
notEmptyCallback?.call();
}
close(); close();
} }
@@ -763,6 +928,7 @@ void setPasswordDialog() async {
errMsg0 = ''; errMsg0 = '';
}); });
}, },
maxLength: maxLength,
), ),
), ),
], ],
@@ -789,6 +955,7 @@ void setPasswordDialog() async {
errMsg1 = ''; errMsg1 = '';
}); });
}, },
maxLength: maxLength,
), ),
), ),
], ],

File diff suppressed because it is too large Load Diff

View File

@@ -1,14 +1,14 @@
import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/desktop/pages/desktop_home_page.dart'; import 'package:flutter_hbb/desktop/pages/desktop_home_page.dart';
import 'package:flutter_hbb/desktop/pages/desktop_setting_page.dart'; import 'package:flutter_hbb/desktop/pages/desktop_setting_page.dart';
import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart'; import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
import 'package:flutter_hbb/models/platform_model.dart';
import 'package:flutter_hbb/models/state_model.dart'; import 'package:flutter_hbb/models/state_model.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:window_manager/window_manager.dart'; import 'package:window_manager/window_manager.dart';
// import 'package:flutter/services.dart';
import '../../common/shared_state.dart'; import '../../common/shared_state.dart';
@@ -18,9 +18,10 @@ class DesktopTabPage extends StatefulWidget {
@override @override
State<DesktopTabPage> createState() => _DesktopTabPageState(); State<DesktopTabPage> createState() => _DesktopTabPageState();
static void onAddSetting({int initialPage = 0}) { static void onAddSetting(
{SettingsTabKey initialPage = SettingsTabKey.general}) {
try { try {
DesktopTabController tabController = Get.find(); DesktopTabController tabController = Get.find<DesktopTabController>();
tabController.add(TabInfo( tabController.add(TabInfo(
key: kTabLabelSettingPage, key: kTabLabelSettingPage,
label: kTabLabelSettingPage, label: kTabLabelSettingPage,
@@ -28,7 +29,7 @@ class DesktopTabPage extends StatefulWidget {
unselectedIcon: Icons.build_outlined, unselectedIcon: Icons.build_outlined,
page: DesktopSettingPage( page: DesktopSettingPage(
key: const ValueKey(kTabLabelSettingPage), key: const ValueKey(kTabLabelSettingPage),
initialPage: initialPage, initialTabkey: initialPage,
))); )));
} catch (e) { } catch (e) {
debugPrintStack(label: '$e'); debugPrintStack(label: '$e');
@@ -36,14 +37,16 @@ class DesktopTabPage extends StatefulWidget {
} }
} }
class _DesktopTabPageState extends State<DesktopTabPage> { class _DesktopTabPageState extends State<DesktopTabPage>
with WidgetsBindingObserver {
final tabController = DesktopTabController(tabType: DesktopTabType.main); final tabController = DesktopTabController(tabType: DesktopTabType.main);
@override final RxBool _block = false.obs;
void initState() { // bool mouseIn = false;
super.initState();
Get.put<DesktopTabController>(tabController); _DesktopTabPageState() {
RemoteCountState.init(); RemoteCountState.init();
Get.put<DesktopTabController>(tabController);
tabController.add(TabInfo( tabController.add(TabInfo(
key: kTabLabelHomePage, key: kTabLabelHomePage,
label: kTabLabelHomePage, label: kTabLabelHomePage,
@@ -53,12 +56,51 @@ class _DesktopTabPageState extends State<DesktopTabPage> {
page: DesktopHomePage( page: DesktopHomePage(
key: const ValueKey(kTabLabelHomePage), key: const ValueKey(kTabLabelHomePage),
))); )));
if (bind.isIncomingOnly()) {
tabController.onSelected = (key) {
if (key == kTabLabelHomePage) {
windowManager.setSize(getIncomingOnlyHomeSize());
setResizable(false);
} else {
windowManager.setSize(getIncomingOnlySettingsSize());
setResizable(true);
}
};
}
} }
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
if (state == AppLifecycleState.resumed) {
shouldBeBlocked(_block, canBeBlocked);
} else if (state == AppLifecycleState.inactive) {}
}
@override
void initState() {
super.initState();
// HardwareKeyboard.instance.addHandler(_handleKeyEvent);
WidgetsBinding.instance.addObserver(this);
}
/*
bool _handleKeyEvent(KeyEvent event) {
if (!mouseIn && event is KeyDownEvent) {
print('key down: ${event.logicalKey}');
shouldBeBlocked(_block, canBeBlocked);
}
return false; // allow it to propagate
}
*/
@override @override
void dispose() { void dispose() {
super.dispose(); // HardwareKeyboard.instance.removeHandler(_handleKeyEvent);
WidgetsBinding.instance.removeObserver(this);
Get.delete<DesktopTabController>(); Get.delete<DesktopTabController>();
super.dispose();
} }
@override @override
@@ -68,18 +110,23 @@ class _DesktopTabPageState extends State<DesktopTabPage> {
backgroundColor: Theme.of(context).colorScheme.background, backgroundColor: Theme.of(context).colorScheme.background,
body: DesktopTab( body: DesktopTab(
controller: tabController, controller: tabController,
tail: ActionIcon( tail: Offstage(
message: 'Settings', offstage: bind.isIncomingOnly() || bind.isDisableSettings(),
icon: IconFont.menu, child: ActionIcon(
onTap: DesktopTabPage.onAddSetting, message: 'Settings',
isClose: false, icon: IconFont.menu,
onTap: DesktopTabPage.onAddSetting,
isClose: false,
),
), ),
blockTab: _block,
))); )));
return Platform.isMacOS || kUseCompatibleUiMode return isMacOS || kUseCompatibleUiMode
? tabWidget ? tabWidget
: Obx( : Obx(
() => DragToResizeArea( () => DragToResizeArea(
resizeEdgeSize: stateGlobal.resizeEdgeSize.value, resizeEdgeSize: stateGlobal.resizeEdgeSize.value,
enableResizeEdges: windowManagerEnableResizeEdges,
child: tabWidget, child: tabWidget,
), ),
); );

View File

@@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'dart:math'; import 'dart:math';
import 'package:extended_text/extended_text.dart';
import 'package:flutter_hbb/desktop/widgets/dragable_divider.dart'; import 'package:flutter_hbb/desktop/widgets/dragable_divider.dart';
import 'package:percent_indicator/percent_indicator.dart'; import 'package:percent_indicator/percent_indicator.dart';
import 'package:desktop_drop/desktop_drop.dart'; import 'package:desktop_drop/desktop_drop.dart';
@@ -16,6 +17,8 @@ import 'package:flutter_hbb/models/file_model.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:wakelock_plus/wakelock_plus.dart'; import 'package:wakelock_plus/wakelock_plus.dart';
import 'package:flutter_hbb/web/dummy.dart'
if (dart.library.html) 'package:flutter_hbb/web/web_unique.dart';
import '../../consts.dart'; import '../../consts.dart';
import '../../desktop/widgets/material_mod_popup_menu.dart' as mod_menu; import '../../desktop/widgets/material_mod_popup_menu.dart' as mod_menu;
@@ -53,20 +56,22 @@ class FileManagerPage extends StatefulWidget {
{Key? key, {Key? key,
required this.id, required this.id,
required this.password, required this.password,
required this.tabController, required this.isSharedPassword,
this.tabController,
this.forceRelay}) this.forceRelay})
: super(key: key); : super(key: key);
final String id; final String id;
final String? password; final String? password;
final bool? isSharedPassword;
final bool? forceRelay; final bool? forceRelay;
final DesktopTabController tabController; final DesktopTabController? tabController;
@override @override
State<StatefulWidget> createState() => _FileManagerPageState(); State<StatefulWidget> createState() => _FileManagerPageState();
} }
class _FileManagerPageState extends State<FileManagerPage> class _FileManagerPageState extends State<FileManagerPage>
with AutomaticKeepAliveClientMixin { with AutomaticKeepAliveClientMixin, WidgetsBindingObserver {
final _mouseFocusScope = Rx<MouseFocusScope>(MouseFocusScope.none); final _mouseFocusScope = Rx<MouseFocusScope>(MouseFocusScope.none);
final _dropMaskVisible = false.obs; // TODO impl drop mask final _dropMaskVisible = false.obs; // TODO impl drop mask
@@ -84,18 +89,26 @@ class _FileManagerPageState extends State<FileManagerPage>
_ffi.start(widget.id, _ffi.start(widget.id,
isFileTransfer: true, isFileTransfer: true,
password: widget.password, password: widget.password,
isSharedPassword: widget.isSharedPassword,
forceRelay: widget.forceRelay); forceRelay: widget.forceRelay);
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
_ffi.dialogManager _ffi.dialogManager
.showLoading(translate('Connecting...'), onCancel: closeConnection); .showLoading(translate('Connecting...'), onCancel: closeConnection);
}); });
Get.put(_ffi, tag: 'ft_${widget.id}'); Get.put<FFI>(_ffi, tag: 'ft_${widget.id}');
if (!Platform.isLinux) { if (!isLinux) {
WakelockPlus.enable(); WakelockPlus.enable();
} }
if (isWeb) {
_ffi.ffiModel.updateEventListener(_ffi.sessionId, widget.id);
}
debugPrint("File manager page init success with id ${widget.id}"); debugPrint("File manager page init success with id ${widget.id}");
_ffi.dialogManager.setOverlayState(_overlayKeyState); _ffi.dialogManager.setOverlayState(_overlayKeyState);
widget.tabController.onSelected?.call(widget.id); // Call onSelected in post frame callback, since we cannot guarantee that the callback will not call setState.
WidgetsBinding.instance.addPostFrameCallback((_) {
widget.tabController?.onSelected?.call(widget.id);
});
WidgetsBinding.instance.addObserver(this);
} }
@override @override
@@ -103,17 +116,26 @@ class _FileManagerPageState extends State<FileManagerPage>
model.close().whenComplete(() { model.close().whenComplete(() {
_ffi.close(); _ffi.close();
_ffi.dialogManager.dismissAll(); _ffi.dialogManager.dismissAll();
if (!Platform.isLinux) { if (!isLinux) {
WakelockPlus.disable(); WakelockPlus.disable();
} }
Get.delete<FFI>(tag: 'ft_${widget.id}'); Get.delete<FFI>(tag: 'ft_${widget.id}');
}); });
WidgetsBinding.instance.removeObserver(this);
super.dispose(); super.dispose();
} }
@override @override
bool get wantKeepAlive => true; bool get wantKeepAlive => true;
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
if (state == AppLifecycleState.resumed) {
jobController.jobTable.refresh();
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
super.build(context); super.build(context);
@@ -123,10 +145,11 @@ class _FileManagerPageState extends State<FileManagerPage>
backgroundColor: Theme.of(context).scaffoldBackgroundColor, backgroundColor: Theme.of(context).scaffoldBackgroundColor,
body: Row( body: Row(
children: [ children: [
Flexible( if (!isWeb)
flex: 3, Flexible(
child: dropArea(FileManagerView( flex: 3,
model.localController, _ffi, _mouseFocusScope))), child: dropArea(FileManagerView(
model.localController, _ffi, _mouseFocusScope))),
Flexible( Flexible(
flex: 3, flex: 3,
child: dropArea(FileManagerView( child: dropArea(FileManagerView(
@@ -167,10 +190,31 @@ class _FileManagerPageState extends State<FileManagerPage>
/// transfer status list /// transfer status list
/// watch transfer status /// watch transfer status
Widget statusList() { Widget statusList() {
Widget getIcon(JobProgress job) {
final color = Theme.of(context).tabBarTheme.labelColor;
switch (job.type) {
case JobType.deleteDir:
case JobType.deleteFile:
return Icon(Icons.delete_outline, color: color);
default:
return Transform.rotate(
angle: isWeb
? job.isRemoteToLocal
? pi / 2
: pi / 2 * 3
: job.isRemoteToLocal
? pi
: 0,
child: Icon(Icons.arrow_forward_ios, color: color),
);
}
}
statusListView(List<JobProgress> jobs) => ListView.builder( statusListView(List<JobProgress> jobs) => ListView.builder(
controller: ScrollController(), controller: ScrollController(),
itemBuilder: (BuildContext context, int index) { itemBuilder: (BuildContext context, int index) {
final item = jobs[index]; final item = jobs[index];
final status = item.getStatus();
return Padding( return Padding(
padding: const EdgeInsets.only(bottom: 5), padding: const EdgeInsets.only(bottom: 5),
child: generateCard( child: generateCard(
@@ -180,15 +224,8 @@ class _FileManagerPageState extends State<FileManagerPage>
Row( Row(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
Transform.rotate( getIcon(item)
angle: item.isRemoteToLocal ? pi : 0, .marginSymmetric(horizontal: 10, vertical: 12),
child: SvgPicture.asset("assets/arrow.svg",
colorFilter: svgColor(
Theme.of(context).tabBarTheme.labelColor)),
).paddingOnly(left: 15),
const SizedBox(
width: 16.0,
),
Expanded( Expanded(
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@@ -197,45 +234,28 @@ class _FileManagerPageState extends State<FileManagerPage>
Tooltip( Tooltip(
waitDuration: Duration(milliseconds: 500), waitDuration: Duration(milliseconds: 500),
message: item.jobName, message: item.jobName,
child: Text( child: ExtendedText(
item.fileName, item.jobName,
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
).paddingSymmetric(vertical: 10), overflowWidget: TextOverflowWidget(
), child: Text("..."),
Text( position: TextOverflowPosition.start),
'${translate("Total")} ${readableFileSize(item.totalSize.toDouble())}',
style: TextStyle(
fontSize: 12,
color: MyTheme.darkGray,
), ),
), ),
Offstage( Tooltip(
offstage: item.state != JobState.inProgress, waitDuration: Duration(milliseconds: 500),
child: Text( message: status,
'${translate("Speed")} ${readableFileSize(item.speed)}/s', child: Text(status,
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
color: MyTheme.darkGray, color: MyTheme.darkGray,
), )).marginOnly(top: 6),
),
), ),
Offstage( Offstage(
offstage: item.state == JobState.inProgress, offstage: item.type != JobType.transfer ||
child: Text( item.state != JobState.inProgress,
translate(
item.display(),
),
style: TextStyle(
fontSize: 12,
color: MyTheme.darkGray,
),
),
),
Offstage(
offstage: item.state != JobState.inProgress,
child: LinearPercentIndicator( child: LinearPercentIndicator(
padding: EdgeInsets.only(right: 15),
animateFromLastPercent: true, animateFromLastPercent: true,
center: Text( center: Text(
'${(item.finishedSize / item.totalSize * 100).toStringAsFixed(0)}%', '${(item.finishedSize / item.totalSize * 100).toStringAsFixed(0)}%',
@@ -245,7 +265,7 @@ class _FileManagerPageState extends State<FileManagerPage>
progressColor: MyTheme.accent, progressColor: MyTheme.accent,
backgroundColor: Theme.of(context).hoverColor, backgroundColor: Theme.of(context).hoverColor,
lineHeight: kDesktopFileTransferRowHeight, lineHeight: kDesktopFileTransferRowHeight,
).paddingSymmetric(vertical: 15), ).paddingSymmetric(vertical: 8),
), ),
], ],
), ),
@@ -256,6 +276,7 @@ class _FileManagerPageState extends State<FileManagerPage>
Offstage( Offstage(
offstage: item.state != JobState.paused, offstage: item.state != JobState.paused,
child: MenuButton( child: MenuButton(
tooltip: translate("Resume"),
onPressed: () { onPressed: () {
jobController.resumeJob(item.id); jobController.resumeJob(item.id);
}, },
@@ -268,7 +289,7 @@ class _FileManagerPageState extends State<FileManagerPage>
), ),
), ),
MenuButton( MenuButton(
padding: EdgeInsets.only(right: 15), tooltip: translate("Delete"),
child: SvgPicture.asset( child: SvgPicture.asset(
"assets/close.svg", "assets/close.svg",
colorFilter: svgColor(Colors.white), colorFilter: svgColor(Colors.white),
@@ -281,11 +302,11 @@ class _FileManagerPageState extends State<FileManagerPage>
hoverColor: MyTheme.accent80, hoverColor: MyTheme.accent80,
), ),
], ],
), ).marginAll(12),
], ],
), ),
], ],
).paddingSymmetric(vertical: 10), ),
), ),
); );
}, },
@@ -469,6 +490,9 @@ class _FileManagerViewState extends State<FileManagerView> {
} }
Widget headTools() { Widget headTools() {
var uploadButtonTapPosition = RelativeRect.fill;
RxBool isUploadFolder =
(bind.mainGetLocalOption(key: 'upload-folder-button') == 'Y').obs;
return Container( return Container(
child: Column( child: Column(
children: [ children: [
@@ -515,6 +539,7 @@ class _FileManagerViewState extends State<FileManagerView> {
Row( Row(
children: [ children: [
MenuButton( MenuButton(
tooltip: translate('Back'),
padding: EdgeInsets.only( padding: EdgeInsets.only(
right: 3, right: 3,
), ),
@@ -534,6 +559,7 @@ class _FileManagerViewState extends State<FileManagerView> {
}, },
), ),
MenuButton( MenuButton(
tooltip: translate('Parent directory'),
child: RotatedBox( child: RotatedBox(
quarterTurns: 3, quarterTurns: 3,
child: SvgPicture.asset( child: SvgPicture.asset(
@@ -598,6 +624,7 @@ class _FileManagerViewState extends State<FileManagerView> {
switch (_locationStatus.value) { switch (_locationStatus.value) {
case LocationStatus.bread: case LocationStatus.bread:
return MenuButton( return MenuButton(
tooltip: translate('Search'),
onPressed: () { onPressed: () {
_locationStatus.value = LocationStatus.fileSearchBar; _locationStatus.value = LocationStatus.fileSearchBar;
Future.delayed( Future.delayed(
@@ -624,6 +651,7 @@ class _FileManagerViewState extends State<FileManagerView> {
); );
case LocationStatus.fileSearchBar: case LocationStatus.fileSearchBar:
return MenuButton( return MenuButton(
tooltip: translate('Clear'),
onPressed: () { onPressed: () {
onSearchText("", isLocal); onSearchText("", isLocal);
_locationStatus.value = LocationStatus.bread; _locationStatus.value = LocationStatus.bread;
@@ -639,6 +667,7 @@ class _FileManagerViewState extends State<FileManagerView> {
} }
}), }),
MenuButton( MenuButton(
tooltip: translate('Refresh File'),
padding: EdgeInsets.only( padding: EdgeInsets.only(
left: 3, left: 3,
), ),
@@ -664,6 +693,7 @@ class _FileManagerViewState extends State<FileManagerView> {
isLocal ? MainAxisAlignment.start : MainAxisAlignment.end, isLocal ? MainAxisAlignment.start : MainAxisAlignment.end,
children: [ children: [
MenuButton( MenuButton(
tooltip: translate('Home'),
padding: EdgeInsets.only( padding: EdgeInsets.only(
right: 3, right: 3,
), ),
@@ -679,11 +709,27 @@ class _FileManagerViewState extends State<FileManagerView> {
hoverColor: Theme.of(context).hoverColor, hoverColor: Theme.of(context).hoverColor,
), ),
MenuButton( MenuButton(
tooltip: translate('Create Folder'),
onPressed: () { onPressed: () {
final name = TextEditingController(); final name = TextEditingController();
String? errorText;
_ffi.dialogManager.show((setState, close, context) { _ffi.dialogManager.show((setState, close, context) {
name.addListener(() {
if (errorText != null) {
setState(() {
errorText = null;
});
}
});
submit() { submit() {
if (name.value.text.isNotEmpty) { if (name.value.text.isNotEmpty) {
if (!PathUtil.validName(name.value.text,
controller.options.value.isWindows)) {
setState(() {
errorText = translate("Invalid folder name");
});
return;
}
controller.createDir(PathUtil.join( controller.createDir(PathUtil.join(
controller.directory.value.path, controller.directory.value.path,
name.value.text, name.value.text,
@@ -715,6 +761,7 @@ class _FileManagerViewState extends State<FileManagerView> {
labelText: translate( labelText: translate(
"Please enter the folder name", "Please enter the folder name",
), ),
errorText: errorText,
), ),
controller: name, controller: name,
autofocus: true, autofocus: true,
@@ -748,6 +795,7 @@ class _FileManagerViewState extends State<FileManagerView> {
hoverColor: Theme.of(context).hoverColor, hoverColor: Theme.of(context).hoverColor,
), ),
Obx(() => MenuButton( Obx(() => MenuButton(
tooltip: translate('Delete'),
onPressed: SelectedItems.valid(selectedItems.items) onPressed: SelectedItems.valid(selectedItems.items)
? () async { ? () async {
await (controller await (controller
@@ -767,6 +815,66 @@ class _FileManagerViewState extends State<FileManagerView> {
], ],
), ),
), ),
if (isWeb)
Obx(() => ElevatedButton.icon(
style: ButtonStyle(
padding: MaterialStateProperty.all<EdgeInsetsGeometry>(
isLocal
? EdgeInsets.only(left: 10)
: EdgeInsets.only(right: 10)),
backgroundColor: MaterialStateProperty.all(
selectedItems.items.isEmpty
? MyTheme.accent80
: MyTheme.accent,
),
),
onPressed: () =>
{webselectFiles(is_folder: isUploadFolder.value)},
label: InkWell(
hoverColor: Colors.transparent,
splashColor: Colors.transparent,
highlightColor: Colors.transparent,
focusColor: Colors.transparent,
onTapDown: (e) {
final x = e.globalPosition.dx;
final y = e.globalPosition.dy;
uploadButtonTapPosition =
RelativeRect.fromLTRB(x, y, x, y);
},
onTap: () async {
final value = await showMenu<bool>(
context: context,
position: uploadButtonTapPosition,
items: [
PopupMenuItem<bool>(
value: false,
child: Text(translate('Upload files')),
),
PopupMenuItem<bool>(
value: true,
child: Text(translate('Upload folder')),
),
]);
if (value != null) {
isUploadFolder.value = value;
bind.mainSetLocalOption(
key: 'upload-folder-button',
value: value ? 'Y' : '');
webselectFiles(is_folder: value);
}
},
child: Icon(Icons.arrow_drop_down),
),
icon: Text(
translate(isUploadFolder.isTrue
? 'Upload folder'
: 'Upload files'),
textAlign: TextAlign.right,
style: TextStyle(
color: Colors.white,
),
).marginOnly(left: 8),
)).marginOnly(left: 16),
Obx(() => ElevatedButton.icon( Obx(() => ElevatedButton.icon(
style: ButtonStyle( style: ButtonStyle(
padding: MaterialStateProperty.all<EdgeInsetsGeometry>( padding: MaterialStateProperty.all<EdgeInsetsGeometry>(
@@ -800,19 +908,22 @@ class _FileManagerViewState extends State<FileManagerView> {
: Colors.white, : Colors.white,
), ),
) )
: RotatedBox( : isWeb
quarterTurns: 2, ? Offstage()
child: SvgPicture.asset( : RotatedBox(
"assets/arrow.svg", quarterTurns: 2,
colorFilter: svgColor(selectedItems.items.isEmpty child: SvgPicture.asset(
? Theme.of(context).brightness == "assets/arrow.svg",
Brightness.light colorFilter: svgColor(
? MyTheme.grayBg selectedItems.items.isEmpty
: MyTheme.darkGray ? Theme.of(context).brightness ==
: Colors.white), Brightness.light
alignment: Alignment.bottomRight, ? MyTheme.grayBg
), : MyTheme.darkGray
), : Colors.white),
alignment: Alignment.bottomRight,
),
),
label: isLocal label: isLocal
? SvgPicture.asset( ? SvgPicture.asset(
"assets/arrow.svg", "assets/arrow.svg",
@@ -824,7 +935,7 @@ class _FileManagerViewState extends State<FileManagerView> {
: Colors.white), : Colors.white),
) )
: Text( : Text(
translate('Receive'), translate(isWeb ? 'Download' : 'Receive'),
style: TextStyle( style: TextStyle(
color: selectedItems.items.isEmpty color: selectedItems.items.isEmpty
? Theme.of(context).brightness == ? Theme.of(context).brightness ==
@@ -879,6 +990,7 @@ class _FileManagerViewState extends State<FileManagerView> {
menuPos = RelativeRect.fromLTRB(x, y, x, y); menuPos = RelativeRect.fromLTRB(x, y, x, y);
}, },
child: MenuButton( child: MenuButton(
tooltip: translate('More'),
onPressed: () => mod_menu.showMenu( onPressed: () => mod_menu.showMenu(
context: context, context: context,
position: menuPos, position: menuPos,
@@ -910,6 +1022,7 @@ class _FileManagerViewState extends State<FileManagerView> {
BuildContext context, ScrollController scrollController) { BuildContext context, ScrollController scrollController) {
final fd = controller.directory.value; final fd = controller.directory.value;
final entries = fd.entries; final entries = fd.entries;
Rx<Entry?> rightClickEntry = Rx(null);
return ListSearchActionListener( return ListSearchActionListener(
node: _keyboardNode, node: _keyboardNode,
@@ -968,16 +1081,70 @@ class _FileManagerViewState extends State<FileManagerView> {
final lastModifiedStr = entry.isDrive final lastModifiedStr = entry.isDrive
? " " ? " "
: "${entry.lastModified().toString().replaceAll(".000", "")} "; : "${entry.lastModified().toString().replaceAll(".000", "")} ";
var secondaryPosition = RelativeRect.fromLTRB(0, 0, 0, 0);
onTap() {
final items = selectedItems;
// handle double click
if (_checkDoubleClick(entry)) {
controller.openDirectory(entry.path);
items.clear();
return;
}
_onSelectedChanged(items, filteredEntries, entry, isLocal);
}
onSecondaryTap() {
final items = [
if (!entry.isDrive &&
versionCmp(_ffi.ffiModel.pi.version, "1.3.0") >= 0)
mod_menu.PopupMenuItem(
child: Text(translate("Rename")),
height: CustomPopupMenuTheme.height,
onTap: () {
controller.renameAction(entry, isLocal);
},
)
];
if (items.isNotEmpty) {
rightClickEntry.value = entry;
final future = mod_menu.showMenu(
context: context,
position: secondaryPosition,
items: items,
);
future.then((value) {
rightClickEntry.value = null;
});
future.onError((error, stackTrace) {
rightClickEntry.value = null;
});
}
}
onSecondaryTapDown(details) {
secondaryPosition = RelativeRect.fromLTRB(
details.globalPosition.dx,
details.globalPosition.dy,
details.globalPosition.dx,
details.globalPosition.dy);
}
return Padding( return Padding(
padding: EdgeInsets.symmetric(vertical: 1), padding: EdgeInsets.symmetric(vertical: 1),
child: Obx(() => Container( child: Obx(() => Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: selectedItems.items.contains(entry) color: selectedItems.items.contains(entry)
? Theme.of(context).hoverColor ? MyTheme.button
: Theme.of(context).cardColor, : Theme.of(context).cardColor,
borderRadius: BorderRadius.all( borderRadius: BorderRadius.all(
Radius.circular(5.0), Radius.circular(5.0),
), ),
border: rightClickEntry.value == entry
? Border.all(
color: MyTheme.button,
width: 1.0,
)
: null,
), ),
key: ValueKey(entry.name), key: ValueKey(entry.name),
height: kDesktopFileTransferRowHeight, height: kDesktopFileTransferRowHeight,
@@ -1016,22 +1183,19 @@ class _FileManagerViewState extends State<FileManagerView> {
), ),
Expanded( Expanded(
child: Text(entry.name.nonBreaking, child: Text(entry.name.nonBreaking,
style: TextStyle(
color: selectedItems.items
.contains(entry)
? Colors.white
: null),
overflow: overflow:
TextOverflow.ellipsis)) TextOverflow.ellipsis))
]), ]),
)), )),
), ),
onTap: () { onTap: onTap,
final items = selectedItems; onSecondaryTap: onSecondaryTap,
// handle double click onSecondaryTapDown: onSecondaryTapDown,
if (_checkDoubleClick(entry)) {
controller.openDirectory(entry.path);
items.clear();
return;
}
_onSelectedChanged(
items, filteredEntries, entry, isLocal);
},
), ),
SizedBox( SizedBox(
width: 2.0, width: 2.0,
@@ -1048,11 +1212,17 @@ class _FileManagerViewState extends State<FileManagerView> {
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
color: MyTheme.darkGray, color: selectedItems.items
.contains(entry)
? Colors.white70
: MyTheme.darkGray,
), ),
)), )),
), ),
), ),
onTap: onTap,
onSecondaryTap: onSecondaryTap,
onSecondaryTapDown: onSecondaryTapDown,
), ),
// Divider from header. // Divider from header.
SizedBox( SizedBox(
@@ -1068,9 +1238,16 @@ class _FileManagerViewState extends State<FileManagerView> {
sizeStr, sizeStr,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: TextStyle( style: TextStyle(
fontSize: 10, color: MyTheme.darkGray), fontSize: 10,
color:
selectedItems.items.contains(entry)
? Colors.white70
: MyTheme.darkGray),
), ),
), ),
onTap: onTap,
onSecondaryTap: onSecondaryTap,
onSecondaryTapDown: onSecondaryTapDown,
), ),
), ),
], ],
@@ -1295,7 +1472,7 @@ class _FileManagerViewState extends State<FileManagerView> {
onPointerSignal: (e) { onPointerSignal: (e) {
if (e is PointerScrollEvent) { if (e is PointerScrollEvent) {
final sc = _breadCrumbScroller; final sc = _breadCrumbScroller;
final scale = Platform.isWindows ? 2 : 4; final scale = isWindows ? 2 : 4;
sc.jumpTo(sc.offset + e.scrollDelta.dy / scale); sc.jumpTo(sc.offset + e.scrollDelta.dy / scale);
} }
}, },

View File

@@ -1,5 +1,4 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:io';
import 'package:desktop_multi_window/desktop_multi_window.dart'; import 'package:desktop_multi_window/desktop_multi_window.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@@ -35,6 +34,7 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
WindowController.fromWindowId(windowId()) WindowController.fromWindowId(windowId())
.setTitle(getWindowNameWithId(id)); .setTitle(getWindowNameWithId(id));
}; };
tabController.onRemoved = (_, id) => onRemoveId(id);
tabController.add(TabInfo( tabController.add(TabInfo(
key: params['id'], key: params['id'],
label: params['id'], label: params['id'],
@@ -45,6 +45,7 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
key: ValueKey(params['id']), key: ValueKey(params['id']),
id: params['id'], id: params['id'],
password: params['password'], password: params['password'],
isSharedPassword: params['isSharedPassword'],
tabController: tabController, tabController: tabController,
forceRelay: params['forceRelay'], forceRelay: params['forceRelay'],
))); )));
@@ -54,8 +55,6 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
void initState() { void initState() {
super.initState(); super.initState();
tabController.onRemoved = (_, id) => onRemoveId(id);
rustDeskWinManager.setMethodHandler((call, fromWindowId) async { rustDeskWinManager.setMethodHandler((call, fromWindowId) async {
print( print(
"[FileTransfer] call ${call.method} with args ${call.arguments} from window $fromWindowId to ${windowId()}"); "[FileTransfer] call ${call.method} with args ${call.arguments} from window $fromWindowId to ${windowId()}");
@@ -74,6 +73,7 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
key: ValueKey(id), key: ValueKey(id),
id: id, id: id,
password: args['password'], password: args['password'],
isSharedPassword: args['isSharedPassword'],
tabController: tabController, tabController: tabController,
forceRelay: args['forceRelay'], forceRelay: args['forceRelay'],
))); )));
@@ -90,23 +90,28 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final tabWidget = Container( final child = Scaffold(
decoration: BoxDecoration( backgroundColor: Theme.of(context).cardColor,
border: Border.all(color: MyTheme.color(context).border!)), body: DesktopTab(
child: Scaffold( controller: tabController,
backgroundColor: Theme.of(context).cardColor, onWindowCloseButton: handleWindowCloseButton,
body: DesktopTab( tail: const AddButton(),
controller: tabController, selectedBorderColor: MyTheme.accent,
onWindowCloseButton: handleWindowCloseButton, labelGetter: DesktopTab.tablabelGetter,
tail: const AddButton().paddingOnly(left: 10), ));
labelGetter: DesktopTab.tablabelGetter, final tabWidget = isLinux
)), ? buildVirtualWindowFrame(context, child)
); : Container(
return Platform.isMacOS || kUseCompatibleUiMode decoration: BoxDecoration(
border: Border.all(color: MyTheme.color(context).border!)),
child: child,
);
return isMacOS || kUseCompatibleUiMode
? tabWidget ? tabWidget
: SubWindowDragToResizeArea( : SubWindowDragToResizeArea(
child: tabWidget, child: tabWidget,
resizeEdgeSize: stateGlobal.resizeEdgeSize.value, resizeEdgeSize: stateGlobal.resizeEdgeSize.value,
enableResizeEdges: subWindowManagerEnableResizeEdges,
windowId: stateGlobal.windowId, windowId: stateGlobal.windowId,
); );
} }
@@ -127,9 +132,9 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
tabController.clear(); tabController.clear();
return true; return true;
} else { } else {
final opt = "enable-confirm-closing-tabs";
final bool res; final bool res;
if (!option2bool(opt, bind.mainGetLocalOption(key: opt))) { if (!option2bool(kOptionEnableConfirmClosingTabs,
bind.mainGetLocalOption(key: kOptionEnableConfirmClosingTabs))) {
res = true; res = true;
} else { } else {
res = await closeConfirmDialog(); res = await closeConfirmDialog();

View File

@@ -1,3 +1,5 @@
import 'dart:convert';
import 'package:file_picker/file_picker.dart'; import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/common.dart';
@@ -19,9 +21,7 @@ class InstallPage extends StatefulWidget {
class _InstallPageState extends State<InstallPage> { class _InstallPageState extends State<InstallPage> {
final tabController = DesktopTabController(tabType: DesktopTabType.main); final tabController = DesktopTabController(tabType: DesktopTabType.main);
@override _InstallPageState() {
void initState() {
super.initState();
Get.put<DesktopTabController>(tabController); Get.put<DesktopTabController>(tabController);
const label = "install"; const label = "install";
tabController.add(TabInfo( tabController.add(TabInfo(
@@ -43,6 +43,7 @@ class _InstallPageState extends State<InstallPage> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return DragToResizeArea( return DragToResizeArea(
resizeEdgeSize: stateGlobal.resizeEdgeSize.value, resizeEdgeSize: stateGlobal.resizeEdgeSize.value,
enableResizeEdges: windowManagerEnableResizeEdges,
child: Container( child: Container(
child: Scaffold( child: Scaffold(
backgroundColor: Theme.of(context).colorScheme.background, backgroundColor: Theme.of(context).colorScheme.background,
@@ -73,10 +74,16 @@ class _InstallPageBodyState extends State<_InstallPageBody>
padding: EdgeInsets.symmetric(vertical: 15, horizontal: 12), padding: EdgeInsets.symmetric(vertical: 15, horizontal: 12),
); );
_InstallPageBodyState() {
controller = TextEditingController(text: bind.installInstallPath());
final installOptions = jsonDecode(bind.installInstallOptions());
startmenu.value = installOptions['STARTMENUSHORTCUTS'] != '0';
desktopicon.value = installOptions['DESKTOPSHORTCUTS'] != '0';
}
@override @override
void initState() { void initState() {
windowManager.addListener(this); windowManager.addListener(this);
controller = TextEditingController(text: bind.installInstallPath());
super.initState(); super.initState();
} }
@@ -248,6 +255,7 @@ class _InstallPageBodyState extends State<_InstallPageBody>
if (desktopicon.value) args += ' desktopicon'; if (desktopicon.value) args += ' desktopicon';
bind.installInstallMe(options: args, path: controller.text); bind.installInstallMe(options: args, path: controller.text);
} }
do_install(); do_install();
} }

View File

@@ -25,19 +25,21 @@ class _PortForward {
} }
class PortForwardPage extends StatefulWidget { class PortForwardPage extends StatefulWidget {
const PortForwardPage( const PortForwardPage({
{Key? key, Key? key,
required this.id, required this.id,
required this.password, required this.password,
required this.tabController, required this.tabController,
required this.isRDP, required this.isRDP,
this.forceRelay}) required this.isSharedPassword,
: super(key: key); this.forceRelay,
}) : super(key: key);
final String id; final String id;
final String? password; final String? password;
final DesktopTabController tabController; final DesktopTabController tabController;
final bool isRDP; final bool isRDP;
final bool? forceRelay; final bool? forceRelay;
final bool? isSharedPassword;
@override @override
State<PortForwardPage> createState() => _PortForwardPageState(); State<PortForwardPage> createState() => _PortForwardPageState();
@@ -58,11 +60,15 @@ class _PortForwardPageState extends State<PortForwardPage>
_ffi.start(widget.id, _ffi.start(widget.id,
isPortForward: true, isPortForward: true,
password: widget.password, password: widget.password,
isSharedPassword: widget.isSharedPassword,
forceRelay: widget.forceRelay, forceRelay: widget.forceRelay,
isRdp: widget.isRDP); isRdp: widget.isRDP);
Get.put(_ffi, tag: 'pf_${widget.id}'); Get.put<FFI>(_ffi, tag: 'pf_${widget.id}');
debugPrint("Port forward page init success with id ${widget.id}"); debugPrint("Port forward page init success with id ${widget.id}");
widget.tabController.onSelected?.call(widget.id); // Call onSelected in post frame callback, since we cannot guarantee that the callback will not call setState.
WidgetsBinding.instance.addPostFrameCallback((_) {
widget.tabController.onSelected?.call(widget.id);
});
} }
@override @override
@@ -138,8 +144,9 @@ class _PortForwardPageState extends State<PortForwardPage>
child: Text(translate(label)).marginOnly(left: _kTextLeftMargin)); child: Text(translate(label)).marginOnly(left: _kTextLeftMargin));
return Theme( return Theme(
data: Theme.of(context) data: Theme.of(context).copyWith(
.copyWith(backgroundColor: Theme.of(context).colorScheme.background), colorScheme: Theme.of(context).colorScheme,
),
child: Obx(() => ListView.builder( child: Obx(() => ListView.builder(
controller: ScrollController(), controller: ScrollController(),
itemCount: pfs.length + 2, itemCount: pfs.length + 2,
@@ -286,7 +293,7 @@ class _PortForwardPageState extends State<PortForwardPage>
).marginOnly(left: _kTextLeftMargin)); ).marginOnly(left: _kTextLeftMargin));
return Theme( return Theme(
data: Theme.of(context) data: Theme.of(context)
.copyWith(backgroundColor: Theme.of(context).colorScheme.background), .copyWith(colorScheme: Theme.of(context).colorScheme),
child: ListView.builder( child: ListView.builder(
controller: ScrollController(), controller: ScrollController(),
itemCount: 2, itemCount: 2,

Some files were not shown because too many files have changed in this diff Show More