From 9f0985c842535a190d6d9d0a098a9c81db112619 Mon Sep 17 00:00:00 2001 From: FastAct <93490087+FastAct@users.noreply.github.com> Date: Fri, 26 Jul 2024 11:17:05 +0200 Subject: [PATCH 001/541] Update nl.rs (#8843) --- src/lang/nl.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lang/nl.rs b/src/lang/nl.rs index a3042b2f2da3..ea0523ba7f0d 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -629,7 +629,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("enable-bot-desc", "1, Open een chat met @BotFather.\n2, Verzend het commando \"/newbot\". Als deze stap voltooid is, ontvang je een token.\n3, Start een chat met de nieuw aangemaakte bot. Om hem te activeren stuurt u een bericht dat begint met een schuine streep (\"/\"), bijvoorbeeld \"/hello\".\n"), ("cancel-2fa-confirm-tip", "Weet je zeker dat je 2FA wilt annuleren?"), ("cancel-bot-confirm-tip", "Weet je zeker dat je de Telegram-bot wilt annuleren?"), - ("About RustDesk", ""), - ("Send clipboard keystrokes", ""), + ("About RustDesk", "Over RustDesk"), + ("Send clipboard keystrokes", "Klembord toetsaanslagen verzenden"), ].iter().cloned().collect(); } From 30a5d1e0e1cd9d4d986efb2dbba90f32481c3fe9 Mon Sep 17 00:00:00 2001 From: 21pages Date: Sat, 27 Jul 2024 09:50:06 +0800 Subject: [PATCH 002/541] avoid call refreshCurrentUser twice at startup (#8848) refreshCurrentUser will be called at these 2 position: 1. runMainApp or runMobileApp in main.dart 2. when connect status is ready Both of these two happens at startup, when connect status is ready and startup time < 5 seconds, not call refreshCurrentUser Signed-off-by: 21pages --- flutter/lib/desktop/pages/connection_page.dart | 4 +++- flutter/lib/models/ab_model.dart | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index 1403d4493471..5b77c431bf2f 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -34,6 +34,7 @@ class _OnlineStatusWidgetState extends State { final _svcStopped = Get.find(tag: 'stop-service'); final _svcIsUsingPublicServer = true.obs; Timer? _updateTimer; + final DateTime _appStartTime = DateTime.now(); double get em => 14.0; double? get height => bind.isIncomingOnly() ? null : em * 3; @@ -176,7 +177,8 @@ class _OnlineStatusWidgetState extends State { stateGlobal.svcStatus.value = SvcStatus.notReady; } else if (statusNum == 1) { stateGlobal.svcStatus.value = SvcStatus.ready; - if (preStatus != SvcStatus.ready) { + if (preStatus != SvcStatus.ready && + DateTime.now().difference(_appStartTime) > Duration(seconds: 5)) { gFFI.userModel.refreshCurrentUser(); } } else { diff --git a/flutter/lib/models/ab_model.dart b/flutter/lib/models/ab_model.dart index 8cd5cc92293d..c3fad6fe4351 100644 --- a/flutter/lib/models/ab_model.dart +++ b/flutter/lib/models/ab_model.dart @@ -111,9 +111,9 @@ class AbModel { Future _pullAb( {required ForcePullAb? force, required bool quiet}) async { if (bind.isDisableAb()) return; - debugPrint("pullAb, force: $force, quiet: $quiet"); if (!gFFI.userModel.isLogin) return; if (force == null && listInitialized && current.initialized) return; + debugPrint("pullAb, force: $force, quiet: $quiet"); if (!listInitialized || force == ForcePullAb.listAndCurrent) { try { // Read personal guid every time to avoid upgrading the server without closing the main window From 1357ef5d6fc0d634724acb0ea95d8ff773bea589 Mon Sep 17 00:00:00 2001 From: Vasyl Gello Date: Sat, 27 Jul 2024 07:32:26 +0000 Subject: [PATCH 003/541] Try to fix sciter-armhf build (#8850) * Bootstrap CMake 3.29.7 for ubuntu/bionic/armhf * vcpkg exevutable now needs GCC 8 but all product binaries are still OK with GCC 7 * Remove res/vcpkg/linux.cmake Signed-off-by: Vasyl Gello --- .github/workflows/flutter-build.yml | 24 ++++++++-- res/vcpkg/linux.cmake | 74 ----------------------------- 2 files changed, 20 insertions(+), 78 deletions(-) delete mode 100644 res/vcpkg/linux.cmake diff --git a/.github/workflows/flutter-build.yml b/.github/workflows/flutter-build.yml index 2669f06d7a4a..8ed5f5d4fe72 100644 --- a/.github/workflows/flutter-build.yml +++ b/.github/workflows/flutter-build.yml @@ -21,6 +21,7 @@ env: WIN_RUST_VERSION: "1.75" # https://github.com/rustdesk/rustdesk/discussions/7503, also 1.78 has ABI change which causes our sciter version not working, https://blog.rust-lang.org/2024/03/30/i128-layout-update.html RUST_VERSION: "1.75" # sciter failed on m1 with 1.78 because of https://blog.rust-lang.org/2024/03/30/i128-layout-update.html CARGO_NDK_VERSION: "3.1.2" + SCITER_ARMV7_CMAKE_VERSION: "3.29.7" LLVM_VERSION: "15.0.6" FLUTTER_VERSION: "3.19.6" ANDROID_FLUTTER_VERSION: "3.13.9" # >= 3.16 is very slow on my android phone, but work well on most of others. We may switch to new flutter after changing to texture rendering (I believe it can solve my problem). @@ -1352,7 +1353,6 @@ jobs: apt-get install -y \ build-essential \ clang \ - cmake \ curl \ gcc \ git \ @@ -1388,6 +1388,18 @@ jobs: wget --output-document nasm.deb "http://ftp.us.debian.org/debian/pool/main/n/nasm/nasm_2.14-1_${{ matrix.job.deb_arch }}.deb" dpkg -i nasm.deb rm -f nasm.deb + # install {gcc,g++}-8 and prebuilt cmake to build vcpkg executable on arm-linux + if [ "${{ matrix.job.vcpkg-triplet }}" = "arm-linux" ]; then + apt-get install -y gcc-8 g++-8 + apt-get install -y libssl-dev + git clone --depth 1 https://github.com/kitware/cmake -b "v${{ env.SCITER_ARMV7_CMAKE_VERSION }}" /tmp/cmake + pushd /tmp/cmake + ./bootstrap --generator='Unix Makefiles' "--prefix=/opt/cmake-${{ env.SCITER_ARMV7_CMAKE_VERSION }}-linux-armhf" --parallel="$(nproc)" + make -j install + popd + rm -rf /tmp/cmake + export PATH="/opt/cmake-${{ env.SCITER_ARMV7_CMAKE_VERSION }}-linux-armhf/bin:$PATH" + fi run: | # disable git safe.directory git config --global --add safe.directory "*" @@ -1400,11 +1412,15 @@ jobs: git clone https://github.com/microsoft/vcpkg pushd vcpkg git reset --hard ${{ env.VCPKG_COMMIT_ID }} - sh bootstrap-vcpkg.sh -disableMetrics + # Build vcpkg helper executable with gcc-8 for arm-linux but use prebuilt one on x64-linux + if [ "${{ matrix.job.vcpkg-triplet }}" = "arm-linux" ]; then + CC=/usr/bin/gcc-8 CXX=/usr/bin/g++-8 sh bootstrap-vcpkg.sh -disableMetrics + export VCPKG_FORCE_SYSTEM_BINARIES=1 + else + sh bootstrap-vcpkg.sh -disableMetrics + fi popd popd - # override Linux compiler detection in vcpkg - cp $PWD/res/vcpkg/linux.cmake $VCPKG_ROOT/scripts/toolchains/linux.cmake $VCPKG_ROOT/vcpkg install --triplet ${{ matrix.job.vcpkg-triplet }} --x-install-root="$VCPKG_ROOT/installed" # rust pushd /opt diff --git a/res/vcpkg/linux.cmake b/res/vcpkg/linux.cmake deleted file mode 100644 index 9496930d0456..000000000000 --- a/res/vcpkg/linux.cmake +++ /dev/null @@ -1,74 +0,0 @@ -if(NOT _VCPKG_LINUX_TOOLCHAIN) -set(_VCPKG_LINUX_TOOLCHAIN 1) -if(CMAKE_HOST_SYSTEM_NAME STREQUAL "Linux") - set(CMAKE_CROSSCOMPILING OFF CACHE BOOL "") -endif() -set(CMAKE_SYSTEM_NAME Linux CACHE STRING "") -if(VCPKG_TARGET_ARCHITECTURE STREQUAL "x64") - set(CMAKE_SYSTEM_PROCESSOR x86_64 CACHE STRING "") -elseif(VCPKG_TARGET_ARCHITECTURE STREQUAL "x86") - set(CMAKE_SYSTEM_PROCESSOR x86 CACHE STRING "") - string(APPEND VCPKG_C_FLAGS " -m32") - string(APPEND VCPKG_CXX_FLAGS " -m32") - string(APPEND VCPKG_LINKER_FLAGS " -m32") -elseif(VCPKG_TARGET_ARCHITECTURE STREQUAL "arm") - set(CMAKE_SYSTEM_PROCESSOR armv7l CACHE STRING "") - if(CMAKE_HOST_SYSTEM_NAME STREQUAL "Linux") - if(NOT DEFINED CMAKE_CXX_COMPILER) - set(CMAKE_CXX_COMPILER "arm-linux-gnueabihf-g++") - endif() - if(NOT DEFINED CMAKE_C_COMPILER) - set(CMAKE_C_COMPILER "arm-linux-gnueabihf-gcc") - endif() - if(NOT DEFINED CMAKE_ASM_COMPILER) - set(CMAKE_ASM_COMPILER "arm-linux-gnueabihf-gcc") - endif() - if(NOT DEFINED CMAKE_ASM-ATT_COMPILER) - set(CMAKE_ASM-ATT_COMPILER "arm-linux-gnueabihf-as") - endif() - message(STATUS "Cross compiling arm on host ${CMAKE_HOST_SYSTEM_PROCESSOR}, use cross compiler: ${CMAKE_CXX_COMPILER}/${CMAKE_C_COMPILER}") - endif() -elseif(VCPKG_TARGET_ARCHITECTURE STREQUAL "arm64") - set(CMAKE_SYSTEM_PROCESSOR aarch64 CACHE STRING "") - if(CMAKE_HOST_SYSTEM_NAME STREQUAL "Linux") - if(NOT DEFINED CMAKE_CXX_COMPILER) - set(CMAKE_CXX_COMPILER "aarch64-linux-gnu-g++") - endif() - if(NOT DEFINED CMAKE_C_COMPILER) - set(CMAKE_C_COMPILER "aarch64-linux-gnu-gcc") - endif() - if(NOT DEFINED CMAKE_ASM_COMPILER) - set(CMAKE_ASM_COMPILER "aarch64-linux-gnu-gcc") - endif() - if(NOT DEFINED CMAKE_ASM-ATT_COMPILER) - set(CMAKE_ASM-ATT_COMPILER "aarch64-linux-gnu-as") - endif() - message(STATUS "Cross compiling arm64 on host ${CMAKE_HOST_SYSTEM_PROCESSOR}, use cross compiler: ${CMAKE_CXX_COMPILER}/${CMAKE_C_COMPILER}") - endif() -endif() - -get_property( _CMAKE_IN_TRY_COMPILE GLOBAL PROPERTY IN_TRY_COMPILE ) -if(NOT _CMAKE_IN_TRY_COMPILE) - string(APPEND CMAKE_C_FLAGS_INIT " -fPIC ${VCPKG_C_FLAGS} ") - string(APPEND CMAKE_CXX_FLAGS_INIT " -fPIC ${VCPKG_CXX_FLAGS} ") - string(APPEND CMAKE_C_FLAGS_DEBUG_INIT " ${VCPKG_C_FLAGS_DEBUG} ") - string(APPEND CMAKE_CXX_FLAGS_DEBUG_INIT " ${VCPKG_CXX_FLAGS_DEBUG} ") - string(APPEND CMAKE_C_FLAGS_RELEASE_INIT " ${VCPKG_C_FLAGS_RELEASE} ") - string(APPEND CMAKE_CXX_FLAGS_RELEASE_INIT " ${VCPKG_CXX_FLAGS_RELEASE} ") - - string(APPEND CMAKE_MODULE_LINKER_FLAGS_INIT " ${VCPKG_LINKER_FLAGS} ") - string(APPEND CMAKE_SHARED_LINKER_FLAGS_INIT " ${VCPKG_LINKER_FLAGS} ") - string(APPEND CMAKE_EXE_LINKER_FLAGS_INIT " ${VCPKG_LINKER_FLAGS} ") - if(VCPKG_CRT_LINKAGE STREQUAL "static") - string(APPEND CMAKE_MODULE_LINKER_FLAGS_INIT "-static ") - string(APPEND CMAKE_SHARED_LINKER_FLAGS_INIT "-static ") - string(APPEND CMAKE_EXE_LINKER_FLAGS_INIT "-static ") - endif() - string(APPEND CMAKE_MODULE_LINKER_FLAGS_DEBUG_INIT " ${VCPKG_LINKER_FLAGS_DEBUG} ") - string(APPEND CMAKE_SHARED_LINKER_FLAGS_DEBUG_INIT " ${VCPKG_LINKER_FLAGS_DEBUG} ") - string(APPEND CMAKE_EXE_LINKER_FLAGS_DEBUG_INIT " ${VCPKG_LINKER_FLAGS_DEBUG} ") - string(APPEND CMAKE_MODULE_LINKER_FLAGS_RELEASE_INIT " ${VCPKG_LINKER_FLAGS_RELEASE} ") - string(APPEND CMAKE_SHARED_LINKER_FLAGS_RELEASE_INIT " ${VCPKG_LINKER_FLAGS_RELEASE} ") - string(APPEND CMAKE_EXE_LINKER_FLAGS_RELEASE_INIT " ${VCPKG_LINKER_FLAGS_RELEASE} ") -endif() -endif() From d679e8fa7d6e713722c226f6f9fcb790eab7a151 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Sat, 27 Jul 2024 16:16:31 +0800 Subject: [PATCH 004/541] WIN_RUST_VERSION -> SCITER_RUST_VERSION --- .github/workflows/flutter-build.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/flutter-build.yml b/.github/workflows/flutter-build.yml index 8ed5f5d4fe72..d5af6dfbba95 100644 --- a/.github/workflows/flutter-build.yml +++ b/.github/workflows/flutter-build.yml @@ -18,7 +18,7 @@ on: # in this file! env: - WIN_RUST_VERSION: "1.75" # https://github.com/rustdesk/rustdesk/discussions/7503, also 1.78 has ABI change which causes our sciter version not working, https://blog.rust-lang.org/2024/03/30/i128-layout-update.html + SCITER_RUST_VERSION: "1.75" # https://github.com/rustdesk/rustdesk/discussions/7503, also 1.78 has ABI change which causes our sciter version not working, https://blog.rust-lang.org/2024/03/30/i128-layout-update.html RUST_VERSION: "1.75" # sciter failed on m1 with 1.78 because of https://blog.rust-lang.org/2024/03/30/i128-layout-update.html CARGO_NDK_VERSION: "3.1.2" SCITER_ARMV7_CMAKE_VERSION: "3.29.7" @@ -94,7 +94,7 @@ jobs: - name: Install Rust toolchain uses: dtolnay/rust-toolchain@v1 with: - toolchain: ${{ env.WIN_RUST_VERSION }} + toolchain: ${{ env.SCITER_RUST_VERSION }} targets: ${{ matrix.job.target }} components: "rustfmt" @@ -1326,7 +1326,7 @@ jobs: - name: Install Rust toolchain uses: dtolnay/rust-toolchain@v1 with: - toolchain: ${{ env.RUST_VERSION }} + toolchain: ${{ env.SCITER_RUST_VERSION }} targets: ${{ matrix.job.target }} components: "rustfmt" From aa42bf548e7f61809347ee28f7f05dcb0ee4f385 Mon Sep 17 00:00:00 2001 From: Vasyl Gello Date: Sat, 27 Jul 2024 08:29:53 +0000 Subject: [PATCH 005/541] Limit cmake buildstep to 1 threads to check OOM (#8852) Signed-off-by: Vasyl Gello --- .github/workflows/flutter-build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/flutter-build.yml b/.github/workflows/flutter-build.yml index d5af6dfbba95..c213837c21b8 100644 --- a/.github/workflows/flutter-build.yml +++ b/.github/workflows/flutter-build.yml @@ -1394,8 +1394,8 @@ jobs: apt-get install -y libssl-dev git clone --depth 1 https://github.com/kitware/cmake -b "v${{ env.SCITER_ARMV7_CMAKE_VERSION }}" /tmp/cmake pushd /tmp/cmake - ./bootstrap --generator='Unix Makefiles' "--prefix=/opt/cmake-${{ env.SCITER_ARMV7_CMAKE_VERSION }}-linux-armhf" --parallel="$(nproc)" - make -j install + ./bootstrap --generator='Unix Makefiles' "--prefix=/opt/cmake-${{ env.SCITER_ARMV7_CMAKE_VERSION }}-linux-armhf" + make -j1 install popd rm -rf /tmp/cmake export PATH="/opt/cmake-${{ env.SCITER_ARMV7_CMAKE_VERSION }}-linux-armhf/bin:$PATH" From f67f2be0cbeca2b1774a1b6ab28d795647856b0f Mon Sep 17 00:00:00 2001 From: Vasyl Gello Date: Sat, 27 Jul 2024 13:28:34 +0000 Subject: [PATCH 006/541] Add built CMake to PATH in actual RD build step (#8855) Signed-off-by: Vasyl Gello --- .github/workflows/flutter-build.yml | 54 +++++++++++++++++------------ 1 file changed, 32 insertions(+), 22 deletions(-) diff --git a/.github/workflows/flutter-build.yml b/.github/workflows/flutter-build.yml index c213837c21b8..1a70c3a437ed 100644 --- a/.github/workflows/flutter-build.yml +++ b/.github/workflows/flutter-build.yml @@ -22,6 +22,7 @@ env: RUST_VERSION: "1.75" # sciter failed on m1 with 1.78 because of https://blog.rust-lang.org/2024/03/30/i128-layout-update.html CARGO_NDK_VERSION: "3.1.2" SCITER_ARMV7_CMAKE_VERSION: "3.29.7" + SCITER_NASM_DEBVERSION: "2.14-1" LLVM_VERSION: "15.0.6" FLUTTER_VERSION: "3.19.6" ANDROID_FLUTTER_VERSION: "3.13.9" # >= 3.16 is very slow on my android phone, but work well on most of others. We may switch to new flutter after changing to texture rendering (I believe it can solve my problem). @@ -1384,14 +1385,12 @@ jobs: wget \ xz-utils \ zip - # install newer nasm for aom - wget --output-document nasm.deb "http://ftp.us.debian.org/debian/pool/main/n/nasm/nasm_2.14-1_${{ matrix.job.deb_arch }}.deb" - dpkg -i nasm.deb - rm -f nasm.deb - # install {gcc,g++}-8 and prebuilt cmake to build vcpkg executable on arm-linux + # arm-linux needs CMake and vcokg built from source as there + # are no prebuilts available from Kitware and Microsoft if [ "${{ matrix.job.vcpkg-triplet }}" = "arm-linux" ]; then - apt-get install -y gcc-8 g++-8 - apt-get install -y libssl-dev + # install gcc/g++ 8 for vcpkg and OpenSSL headers for CMake + apt-get install -y gcc-8 g++-8 libssl-dev + # bootstrap CMake amd add it to PATH git clone --depth 1 https://github.com/kitware/cmake -b "v${{ env.SCITER_ARMV7_CMAKE_VERSION }}" /tmp/cmake pushd /tmp/cmake ./bootstrap --generator='Unix Makefiles' "--prefix=/opt/cmake-${{ env.SCITER_ARMV7_CMAKE_VERSION }}-linux-armhf" @@ -1400,19 +1399,14 @@ jobs: rm -rf /tmp/cmake export PATH="/opt/cmake-${{ env.SCITER_ARMV7_CMAKE_VERSION }}-linux-armhf/bin:$PATH" fi - run: | - # disable git safe.directory - git config --global --add safe.directory "*" - # Set python3.7 as default python3 - update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.7 1 - # vcpkg + # bootstrap vcpkg and set VCPKG_ROOT export VCPKG_ROOT=/opt/artifacts/vcpkg pushd /opt/artifacts rm -rf vcpkg git clone https://github.com/microsoft/vcpkg pushd vcpkg git reset --hard ${{ env.VCPKG_COMMIT_ID }} - # Build vcpkg helper executable with gcc-8 for arm-linux but use prebuilt one on x64-linux + # build vcpkg helper executable with gcc-8 for arm-linux but use prebuilt one on x64-linux if [ "${{ matrix.job.vcpkg-triplet }}" = "arm-linux" ]; then CC=/usr/bin/gcc-8 CXX=/usr/bin/g++-8 sh bootstrap-vcpkg.sh -disableMetrics export VCPKG_FORCE_SYSTEM_BINARIES=1 @@ -1421,28 +1415,44 @@ jobs: fi popd popd - $VCPKG_ROOT/vcpkg install --triplet ${{ matrix.job.vcpkg-triplet }} --x-install-root="$VCPKG_ROOT/installed" # rust pushd /opt # do not use rustup, because memory overflow in qemu - wget -O rust.tar.gz https://static.rust-lang.org/dist/rust-${{env.RUST_TOOLCHAIN_VERSION}}-${{ matrix.job.target }}.tar.gz + wget --output-document rust.tar.gz https://static.rust-lang.org/dist/rust-${{env.RUST_TOOLCHAIN_VERSION}}-${{ matrix.job.target }}.tar.gz tar -zxvf rust.tar.gz > /dev/null && rm rust.tar.gz - cd rust-${{env.RUST_TOOLCHAIN_VERSION}}-${{ matrix.job.target }} && ./install.sh + pushd rust-${{env.RUST_TOOLCHAIN_VERSION}}-${{ matrix.job.target }} + ./install.sh + popd rm -rf rust-${{env.RUST_TOOLCHAIN_VERSION}}-${{ matrix.job.target }} - # edit config + popd + # install newer nasm for aom + wget --output-document nasm.deb "http://ftp.us.debian.org/debian/pool/main/n/nasm/nasm_${{ env.SCITER_NASM_DEBVERSION }}_${{ matrix.job.deb_arch }}.deb" + dpkg -i nasm.deb + rm -f nasm.deb + run: | + # disable git safe.directory + git config --global --add safe.directory "*" + # set python3.7 as default python3 + update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.7 1 + # add built CMake to PATH for arm-linux + if [ "${{ matrix.job.vcpkg-triplet }}" = "arm-linux" ]; then + export PATH="/opt/cmake-${{ env.SCITER_ARMV7_CMAKE_VERSION }}-linux-armhf/bin:$PATH" + fi + # edit cargo config mkdir -p ~/.cargo/ echo """ [source.crates-io] registry = 'https://github.com/rust-lang/crates.io-index' """ > ~/.cargo/config cat ~/.cargo/config - - # build - pushd /workspace + # install dependencies from vcpkg + export VCPKG_ROOT=/opt/artifacts/vcpkg + $VCPKG_ROOT/vcpkg install --triplet ${{ matrix.job.vcpkg-triplet }} --x-install-root="$VCPKG_ROOT/installed" + # build rustdesk python3 ./res/inline-sciter.py export CARGO_INCREMENTAL=0 cargo build --features inline${{ matrix.job.extra_features }} --release --bins --jobs 1 - # package + # make debian package mkdir -p ./Release mv ./target/release/rustdesk ./Release/rustdesk wget -O ./Release/libsciter-gtk.so https://github.com/c-smile/sciter-sdk/raw/master/bin.lnx/${{ matrix.job.sciter_arch }}/libsciter-gtk.so From 67d4e061fba73669990005ed2bc096f5917276b7 Mon Sep 17 00:00:00 2001 From: Vasyl Gello Date: Sat, 27 Jul 2024 15:57:53 +0000 Subject: [PATCH 007/541] Try to detach /opt/artifacts from sciter (#8857) Signed-off-by: Vasyl Gello --- .github/workflows/flutter-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/flutter-build.yml b/.github/workflows/flutter-build.yml index 1a70c3a437ed..1d0c33e32d54 100644 --- a/.github/workflows/flutter-build.yml +++ b/.github/workflows/flutter-build.yml @@ -1347,7 +1347,6 @@ jobs: ls -l "${PWD}" dockerRunArgs: | --volume "${PWD}:/workspace" - --volume "/opt/artifacts:/opt/artifacts" shell: /bin/bash install: | apt-get update @@ -1401,6 +1400,7 @@ jobs: fi # bootstrap vcpkg and set VCPKG_ROOT export VCPKG_ROOT=/opt/artifacts/vcpkg + mkdir -p /opt/artifacts pushd /opt/artifacts rm -rf vcpkg git clone https://github.com/microsoft/vcpkg From 9750e1409cf301b2d3a0681e97a9cd560a876620 Mon Sep 17 00:00:00 2001 From: Vasyl Gello Date: Sun, 28 Jul 2024 00:44:16 +0000 Subject: [PATCH 008/541] Use VCPKG_FORCE_SYSTEM_BINARIES in actual RD sciter build step (#8859) Signed-off-by: Vasyl Gello --- .github/workflows/flutter-build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/flutter-build.yml b/.github/workflows/flutter-build.yml index 1d0c33e32d54..e4bf8eaf6a59 100644 --- a/.github/workflows/flutter-build.yml +++ b/.github/workflows/flutter-build.yml @@ -1409,7 +1409,6 @@ jobs: # build vcpkg helper executable with gcc-8 for arm-linux but use prebuilt one on x64-linux if [ "${{ matrix.job.vcpkg-triplet }}" = "arm-linux" ]; then CC=/usr/bin/gcc-8 CXX=/usr/bin/g++-8 sh bootstrap-vcpkg.sh -disableMetrics - export VCPKG_FORCE_SYSTEM_BINARIES=1 else sh bootstrap-vcpkg.sh -disableMetrics fi @@ -1434,9 +1433,10 @@ jobs: git config --global --add safe.directory "*" # set python3.7 as default python3 update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.7 1 - # add built CMake to PATH for arm-linux + # add built CMake to PATH and set VCPKG_FORCE_SYSTEM_BINARIES Afor arm-linux if [ "${{ matrix.job.vcpkg-triplet }}" = "arm-linux" ]; then export PATH="/opt/cmake-${{ env.SCITER_ARMV7_CMAKE_VERSION }}-linux-armhf/bin:$PATH" + export VCPKG_FORCE_SYSTEM_BINARIES=1 fi # edit cargo config mkdir -p ~/.cargo/ From 7e8d3bd2acb299f6f896cb4adaacb86a59f78d7b Mon Sep 17 00:00:00 2001 From: 21pages Date: Sun, 28 Jul 2024 10:15:09 +0800 Subject: [PATCH 009/541] remove calling refreshCurrentUser when connect status become ready (#8849) When refreshCurrentUser throw error, show check network in ab and group tab. Signed-off-by: 21pages --- flutter/lib/common.dart | 17 +++++++++++ flutter/lib/common/widgets/address_book.dart | 2 ++ flutter/lib/common/widgets/my_group.dart | 2 ++ .../lib/desktop/pages/connection_page.dart | 6 ---- flutter/lib/models/ab_model.dart | 1 + flutter/lib/models/group_model.dart | 1 + flutter/lib/models/user_model.dart | 30 ++++++++++++++----- src/lang/ar.rs | 1 + src/lang/be.rs | 1 + src/lang/bg.rs | 1 + src/lang/ca.rs | 1 + src/lang/cn.rs | 1 + src/lang/cs.rs | 1 + src/lang/da.rs | 1 + src/lang/de.rs | 1 + src/lang/el.rs | 1 + src/lang/en.rs | 1 + src/lang/eo.rs | 1 + src/lang/es.rs | 1 + src/lang/et.rs | 1 + src/lang/eu.rs | 1 + src/lang/fa.rs | 1 + src/lang/fr.rs | 1 + src/lang/he.rs | 1 + src/lang/hr.rs | 1 + src/lang/hu.rs | 1 + src/lang/id.rs | 1 + src/lang/it.rs | 1 + src/lang/ja.rs | 1 + src/lang/ko.rs | 1 + src/lang/kz.rs | 1 + src/lang/lt.rs | 1 + src/lang/lv.rs | 1 + src/lang/nb.rs | 1 + src/lang/nl.rs | 1 + src/lang/pl.rs | 1 + src/lang/pt_PT.rs | 1 + src/lang/ptbr.rs | 1 + src/lang/ro.rs | 1 + src/lang/ru.rs | 1 + src/lang/sk.rs | 1 + src/lang/sl.rs | 1 + src/lang/sq.rs | 1 + src/lang/sr.rs | 1 + src/lang/sv.rs | 1 + src/lang/template.rs | 1 + src/lang/th.rs | 1 + src/lang/tr.rs | 1 + src/lang/tw.rs | 1 + src/lang/ua.rs | 1 + src/lang/vn.rs | 1 + 51 files changed, 90 insertions(+), 13 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index eba626343d03..b2d6515b7fc2 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -3490,3 +3490,20 @@ disableWindowMovable(int? windowId) { WindowController.fromWindowId(windowId).setMovable(false); } } + +Widget netWorkErrorWidget() { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text(translate("network_error_tip")), + ElevatedButton( + onPressed: gFFI.userModel.refreshCurrentUser, + child: Text(translate("Retry"))) + .marginSymmetric(vertical: 16), + Text(gFFI.userModel.networkError.value, + style: TextStyle(fontSize: 11, color: Colors.red)), + ], + )); +} diff --git a/flutter/lib/common/widgets/address_book.dart b/flutter/lib/common/widgets/address_book.dart index 52b2e3d6296a..67b262cd11ac 100644 --- a/flutter/lib/common/widgets/address_book.dart +++ b/flutter/lib/common/widgets/address_book.dart @@ -41,6 +41,8 @@ class _AddressBookState extends State { return Center( child: ElevatedButton( onPressed: loginDialog, child: Text(translate("Login")))); + } else if (gFFI.userModel.networkError.isNotEmpty) { + return netWorkErrorWidget(); } else { return Column( children: [ diff --git a/flutter/lib/common/widgets/my_group.dart b/flutter/lib/common/widgets/my_group.dart index e139ce700d22..0d9cc007ccc0 100644 --- a/flutter/lib/common/widgets/my_group.dart +++ b/flutter/lib/common/widgets/my_group.dart @@ -30,6 +30,8 @@ class _MyGroupState extends State { return Center( child: ElevatedButton( onPressed: loginDialog, child: Text(translate("Login")))); + } else if (gFFI.userModel.networkError.isNotEmpty) { + return netWorkErrorWidget(); } else if (gFFI.groupModel.groupLoading.value && gFFI.groupModel.emtpy) { return const Center( child: CircularProgressIndicator(), diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index 5b77c431bf2f..09ec3418bf3c 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -34,7 +34,6 @@ class _OnlineStatusWidgetState extends State { final _svcStopped = Get.find(tag: 'stop-service'); final _svcIsUsingPublicServer = true.obs; Timer? _updateTimer; - final DateTime _appStartTime = DateTime.now(); double get em => 14.0; double? get height => bind.isIncomingOnly() ? null : em * 3; @@ -170,17 +169,12 @@ class _OnlineStatusWidgetState extends State { final status = jsonDecode(await bind.mainGetConnectStatus()) as Map; 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 && - DateTime.now().difference(_appStartTime) > Duration(seconds: 5)) { - gFFI.userModel.refreshCurrentUser(); - } } else { stateGlobal.svcStatus.value = SvcStatus.notReady; } diff --git a/flutter/lib/models/ab_model.dart b/flutter/lib/models/ab_model.dart index c3fad6fe4351..6f3820e86bb0 100644 --- a/flutter/lib/models/ab_model.dart +++ b/flutter/lib/models/ab_model.dart @@ -112,6 +112,7 @@ class AbModel { {required ForcePullAb? force, required bool quiet}) async { if (bind.isDisableAb()) return; if (!gFFI.userModel.isLogin) return; + if (gFFI.userModel.networkError.isNotEmpty) return; if (force == null && listInitialized && current.initialized) return; debugPrint("pullAb, force: $force, quiet: $quiet"); if (!listInitialized || force == ForcePullAb.listAndCurrent) { diff --git a/flutter/lib/models/group_model.dart b/flutter/lib/models/group_model.dart index 50459ffe90eb..184c94bfff31 100644 --- a/flutter/lib/models/group_model.dart +++ b/flutter/lib/models/group_model.dart @@ -28,6 +28,7 @@ class GroupModel { Future pull({force = true, quiet = false}) async { if (bind.isDisableGroupPanel()) return; if (!gFFI.userModel.isLogin || groupLoading.value) return; + if (gFFI.userModel.networkError.isNotEmpty) return; if (!force && initialized) return; if (!quiet) { groupLoading.value = true; diff --git a/flutter/lib/models/user_model.dart b/flutter/lib/models/user_model.dart index 4e7f881ad0f6..9d9c762d9981 100644 --- a/flutter/lib/models/user_model.dart +++ b/flutter/lib/models/user_model.dart @@ -17,13 +17,23 @@ bool refreshingUser = false; class UserModel { final RxString userName = ''.obs; final RxBool isAdmin = false.obs; + final RxString networkError = ''.obs; bool get isLogin => userName.isNotEmpty; WeakReference parent; - UserModel(this.parent); + UserModel(this.parent) { + userName.listen((p0) { + // When user name becomes empty, show login button + // When user name becomes non-empty: + // For _updateLocalUserInfo, network error will be set later + // For login success, should clear network error + networkError.value = ''; + }); + } void refreshCurrentUser() async { if (bind.isDisableAccount()) return; + networkError.value = ''; final token = bind.mainGetLocalOption(key: 'access_token'); if (token == '') { await updateOtherModels(); @@ -38,12 +48,18 @@ class UserModel { if (refreshingUser) return; try { refreshingUser = true; - final response = await http.post(Uri.parse('$url/api/currentUser'), - headers: { - 'Content-Type': 'application/json', - 'Authorization': 'Bearer $token' - }, - body: json.encode(body)); + final http.Response response; + try { + response = await http.post(Uri.parse('$url/api/currentUser'), + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer $token' + }, + body: json.encode(body)); + } catch (e) { + networkError.value = e.toString(); + rethrow; + } refreshingUser = false; final status = response.statusCode; if (status == 401 || status == 400) { diff --git a/src/lang/ar.rs b/src/lang/ar.rs index 7fd258b81a1b..05b8ae546403 100644 --- a/src/lang/ar.rs +++ b/src/lang/ar.rs @@ -631,5 +631,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("cancel-bot-confirm-tip", ""), ("About RustDesk", ""), ("Send clipboard keystrokes", ""), + ("network_error_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/be.rs b/src/lang/be.rs index 78ed849ff9ac..09936fb5a6ac 100644 --- a/src/lang/be.rs +++ b/src/lang/be.rs @@ -631,5 +631,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("cancel-bot-confirm-tip", ""), ("About RustDesk", ""), ("Send clipboard keystrokes", ""), + ("network_error_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/bg.rs b/src/lang/bg.rs index cddbddac7e6c..d4099fa24279 100644 --- a/src/lang/bg.rs +++ b/src/lang/bg.rs @@ -631,5 +631,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("cancel-bot-confirm-tip", ""), ("About RustDesk", ""), ("Send clipboard keystrokes", ""), + ("network_error_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ca.rs b/src/lang/ca.rs index f376d91df6e1..6b3c4c294ac6 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -631,5 +631,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("cancel-bot-confirm-tip", ""), ("About RustDesk", ""), ("Send clipboard keystrokes", ""), + ("network_error_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index df9fb5303212..2b33a853470a 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -631,5 +631,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("cancel-bot-confirm-tip", "确定要取消 Telegram 机器人吗?"), ("About RustDesk", ""), ("Send clipboard keystrokes", ""), + ("network_error_tip", "请检查网络连接, 然后点击再试"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index a85a4d560b81..574c38297427 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -631,5 +631,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("cancel-bot-confirm-tip", "Jste si jisti, že chcete zrušit bota Telegramu?"), ("About RustDesk", ""), ("Send clipboard keystrokes", ""), + ("network_error_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index 679394599e5b..5ee83337fa28 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -631,5 +631,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("cancel-bot-confirm-tip", ""), ("About RustDesk", ""), ("Send clipboard keystrokes", ""), + ("network_error_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index c604167ef9ff..3a2e78a7a0ab 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -631,5 +631,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("cancel-bot-confirm-tip", "Sind Sie sicher, dass Sie Telegram-Bot abbrechen möchten?"), ("About RustDesk", "Über RustDesk"), ("Send clipboard keystrokes", ""), + ("network_error_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/el.rs b/src/lang/el.rs index 731e2a5f3ce0..c5ed30ebd002 100644 --- a/src/lang/el.rs +++ b/src/lang/el.rs @@ -631,5 +631,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("cancel-bot-confirm-tip", "Είστε βέβαιοι ότι θέλετε να ακυρώσετε το Telegram bot;"), ("About RustDesk", ""), ("Send clipboard keystrokes", ""), + ("network_error_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/en.rs b/src/lang/en.rs index c4045fd8a58a..638c1a608042 100644 --- a/src/lang/en.rs +++ b/src/lang/en.rs @@ -232,5 +232,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("cancel-2fa-confirm-tip", "Are you sure you want to cancel 2FA?"), ("cancel-bot-confirm-tip", "Are you sure you want to cancel Telegram bot?"), ("About RustDesk", ""), + ("network_error_tip", "Please check your network connection, then click retry.") ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 190c693468d1..f92c706c6d22 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -631,5 +631,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("cancel-bot-confirm-tip", ""), ("About RustDesk", ""), ("Send clipboard keystrokes", ""), + ("network_error_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index 0e94071e4a15..e5eaadf3bc98 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -631,5 +631,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("cancel-bot-confirm-tip", "¿Seguro que quieres cancelar el bot de Telegram?"), ("About RustDesk", ""), ("Send clipboard keystrokes", ""), + ("network_error_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/et.rs b/src/lang/et.rs index 81fc2e77a213..bf5742786f87 100644 --- a/src/lang/et.rs +++ b/src/lang/et.rs @@ -631,5 +631,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("cancel-bot-confirm-tip", ""), ("About RustDesk", ""), ("Send clipboard keystrokes", ""), + ("network_error_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eu.rs b/src/lang/eu.rs index 48efcf789bc0..487ee23c31a0 100644 --- a/src/lang/eu.rs +++ b/src/lang/eu.rs @@ -631,5 +631,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("cancel-bot-confirm-tip", ""), ("About RustDesk", ""), ("Send clipboard keystrokes", ""), + ("network_error_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index e6754205a5bd..e7a5af76864f 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -631,5 +631,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("cancel-bot-confirm-tip", ""), ("About RustDesk", ""), ("Send clipboard keystrokes", ""), + ("network_error_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index e0b0306dd74b..bf40cd045260 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -631,5 +631,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("cancel-bot-confirm-tip", ""), ("About RustDesk", ""), ("Send clipboard keystrokes", ""), + ("network_error_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/he.rs b/src/lang/he.rs index 401b52c28830..84e461c06daa 100644 --- a/src/lang/he.rs +++ b/src/lang/he.rs @@ -631,5 +631,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("cancel-bot-confirm-tip", ""), ("About RustDesk", ""), ("Send clipboard keystrokes", ""), + ("network_error_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hr.rs b/src/lang/hr.rs index 778eeacfa67b..aa08949b2084 100644 --- a/src/lang/hr.rs +++ b/src/lang/hr.rs @@ -631,5 +631,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("cancel-bot-confirm-tip", ""), ("About RustDesk", ""), ("Send clipboard keystrokes", ""), + ("network_error_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 21e898c21c6b..054c333e96a6 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -631,5 +631,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("cancel-bot-confirm-tip", ""), ("About RustDesk", ""), ("Send clipboard keystrokes", ""), + ("network_error_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index e13c088fde48..773a75eab549 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -631,5 +631,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("cancel-bot-confirm-tip", ""), ("About RustDesk", ""), ("Send clipboard keystrokes", ""), + ("network_error_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index 5231ddac48a5..39ceeec67a72 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -631,5 +631,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("cancel-bot-confirm-tip", "Sei sicuro di voler annulare Telegram?"), ("About RustDesk", "Info su RustDesk"), ("Send clipboard keystrokes", "Invia sequenze tasti appunti"), + ("network_error_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index a1413f82b892..c2537ebc9d36 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -631,5 +631,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("cancel-bot-confirm-tip", "本当にTelegram Botをキャンセルしますか?"), ("About RustDesk", "RustDeskについて"), ("Send clipboard keystrokes", ""), + ("network_error_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 48b39b4a7081..3d96ce0f2dfa 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -631,5 +631,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("cancel-bot-confirm-tip", ""), ("About RustDesk", ""), ("Send clipboard keystrokes", ""), + ("network_error_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index a8f7fde92d75..21ef3957499f 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -631,5 +631,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("cancel-bot-confirm-tip", ""), ("About RustDesk", ""), ("Send clipboard keystrokes", ""), + ("network_error_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lt.rs b/src/lang/lt.rs index 7167663a52a0..61ff2ee1021e 100644 --- a/src/lang/lt.rs +++ b/src/lang/lt.rs @@ -631,5 +631,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("cancel-bot-confirm-tip", ""), ("About RustDesk", ""), ("Send clipboard keystrokes", ""), + ("network_error_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lv.rs b/src/lang/lv.rs index b8add4aa86d6..5ce48429de32 100644 --- a/src/lang/lv.rs +++ b/src/lang/lv.rs @@ -631,5 +631,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("cancel-bot-confirm-tip", "Vai tiešām vēlaties atcelt Telegram robotu?"), ("About RustDesk", ""), ("Send clipboard keystrokes", ""), + ("network_error_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nb.rs b/src/lang/nb.rs index 36269a377084..3a986ecbb8c4 100644 --- a/src/lang/nb.rs +++ b/src/lang/nb.rs @@ -631,5 +631,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("cancel-bot-confirm-tip", ""), ("About RustDesk", ""), ("Send clipboard keystrokes", ""), + ("network_error_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nl.rs b/src/lang/nl.rs index ea0523ba7f0d..4f4fbcc2bcc8 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -631,5 +631,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("cancel-bot-confirm-tip", "Weet je zeker dat je de Telegram-bot wilt annuleren?"), ("About RustDesk", "Over RustDesk"), ("Send clipboard keystrokes", "Klembord toetsaanslagen verzenden"), + ("network_error_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 3e3d135a7632..370cf59e4e6d 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -631,5 +631,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("cancel-bot-confirm-tip", ""), ("About RustDesk", ""), ("Send clipboard keystrokes", ""), + ("network_error_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index 0192d1a44e0b..0bf0c7304d0f 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -631,5 +631,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("cancel-bot-confirm-tip", ""), ("About RustDesk", ""), ("Send clipboard keystrokes", ""), + ("network_error_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index 5a7646bdc880..003786c462b9 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -631,5 +631,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("cancel-bot-confirm-tip", ""), ("About RustDesk", ""), ("Send clipboard keystrokes", ""), + ("network_error_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ro.rs b/src/lang/ro.rs index fba6707317c6..bace4107288c 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -631,5 +631,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("cancel-bot-confirm-tip", ""), ("About RustDesk", ""), ("Send clipboard keystrokes", ""), + ("network_error_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index c9a1b41eaf26..7ccf6ba6f9f7 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -631,5 +631,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("cancel-bot-confirm-tip", "Отключить Telegram-бота?"), ("About RustDesk", ""), ("Send clipboard keystrokes", ""), + ("network_error_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 6d0eb2ac97d9..9eaed3875b30 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -631,5 +631,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("cancel-bot-confirm-tip", "Ste si istí, že chcete zrušiť bota Telegramu?"), ("About RustDesk", ""), ("Send clipboard keystrokes", ""), + ("network_error_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sl.rs b/src/lang/sl.rs index ef0c0404bcbe..98f33956ac3b 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -631,5 +631,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("cancel-bot-confirm-tip", ""), ("About RustDesk", ""), ("Send clipboard keystrokes", ""), + ("network_error_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sq.rs b/src/lang/sq.rs index 44de8fc20703..c3ebca6290c3 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -631,5 +631,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("cancel-bot-confirm-tip", ""), ("About RustDesk", ""), ("Send clipboard keystrokes", ""), + ("network_error_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sr.rs b/src/lang/sr.rs index 56b6afb49aed..c6773955af85 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -631,5 +631,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("cancel-bot-confirm-tip", ""), ("About RustDesk", ""), ("Send clipboard keystrokes", ""), + ("network_error_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sv.rs b/src/lang/sv.rs index 0e99407c1f63..2445bc2f11bc 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -631,5 +631,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("cancel-bot-confirm-tip", ""), ("About RustDesk", ""), ("Send clipboard keystrokes", ""), + ("network_error_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index b1559a65eca0..3d3c86e5b01d 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -631,5 +631,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("cancel-bot-confirm-tip", ""), ("About RustDesk", ""), ("Send clipboard keystrokes", ""), + ("network_error_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/th.rs b/src/lang/th.rs index 6237b482245f..fa369336cfd1 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -631,5 +631,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("cancel-bot-confirm-tip", ""), ("About RustDesk", ""), ("Send clipboard keystrokes", ""), + ("network_error_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index e1180ba44d98..366265a57c9f 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -631,5 +631,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("cancel-bot-confirm-tip", ""), ("About RustDesk", ""), ("Send clipboard keystrokes", ""), + ("network_error_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 40b9201e15e2..9723bfde59e8 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -631,5 +631,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("cancel-bot-confirm-tip", "確定要取消 Telegram 機器人嗎?"), ("About RustDesk", ""), ("Send clipboard keystrokes", ""), + ("network_error_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ua.rs b/src/lang/ua.rs index d9dc28fe7983..187603fb08c7 100644 --- a/src/lang/ua.rs +++ b/src/lang/ua.rs @@ -631,5 +631,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("cancel-bot-confirm-tip", ""), ("About RustDesk", ""), ("Send clipboard keystrokes", ""), + ("network_error_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index f68786b836a3..6042a98d516d 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -631,5 +631,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("cancel-bot-confirm-tip", ""), ("About RustDesk", ""), ("Send clipboard keystrokes", ""), + ("network_error_tip", ""), ].iter().cloned().collect(); } From ee5314de20044e96e349b2ef3c421f893472d989 Mon Sep 17 00:00:00 2001 From: 21pages Date: Sun, 28 Jul 2024 11:13:19 +0800 Subject: [PATCH 010/541] use selectableText for some errors (#8862) Signed-off-by: 21pages --- flutter/lib/common.dart | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index b2d6515b7fc2..054b4df310f1 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -1214,7 +1214,8 @@ Widget msgboxContent(String type, String title, String text) { translate(title), style: TextStyle(fontSize: 21), ).marginOnly(bottom: 10), - Text(translateText(text), style: const TextStyle(fontSize: 15)), + SelectableText(translateText(text), + style: const TextStyle(fontSize: 15)), ], ), ), @@ -2813,7 +2814,7 @@ Widget buildErrorBanner(BuildContext context, alignment: Alignment.centerLeft, child: Tooltip( message: translate(err.value), - child: Text( + child: SelectableText( translate(err.value), ), )).marginSymmetric(vertical: 2), @@ -3502,7 +3503,7 @@ Widget netWorkErrorWidget() { onPressed: gFFI.userModel.refreshCurrentUser, child: Text(translate("Retry"))) .marginSymmetric(vertical: 16), - Text(gFFI.userModel.networkError.value, + SelectableText(gFFI.userModel.networkError.value, style: TextStyle(fontSize: 11, color: Colors.red)), ], )); From 0faf82f109cfe224cd55ce60b87b1f52a401927a Mon Sep 17 00:00:00 2001 From: bovirus <1262554+bovirus@users.noreply.github.com> Date: Sun, 28 Jul 2024 11:13:01 +0200 Subject: [PATCH 011/541] Update Italian language (#8863) --- src/lang/it.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lang/it.rs b/src/lang/it.rs index 39ceeec67a72..329db79daa4b 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -625,12 +625,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Volume down", "Volume -"), ("Power", "Alimentazione"), ("Telegram bot", "Bot Telgram"), - ("enable-bot-tip", "If you enable this feature, you can receive the 2FA code from your bot. It can also function as a connection notification."), + ("enable-bot-tip", "Se abiliti questa funzione, puoi ricevere il codice 2FA dal tuo bot.\nPuò anche funzionare come notifica di connessione."), ("enable-bot-desc", "1. apri una chat con @BotFather.\n2. Invia il comando \"/newbot\", dopo aver completato questo passaggio riceverai un token.\n3. Avvia una chat con il tuo bot appena creato. Per attivarlo Invia un messaggio che inizia con una barra (\"/\") tipo \"/hello\".\n"), ("cancel-2fa-confirm-tip", "Sei sicuro di voler annullare 2FA?"), - ("cancel-bot-confirm-tip", "Sei sicuro di voler annulare Telegram?"), + ("cancel-bot-confirm-tip", "Sei sicuro di voler annullare Telegram?"), ("About RustDesk", "Info su RustDesk"), ("Send clipboard keystrokes", "Invia sequenze tasti appunti"), - ("network_error_tip", ""), + ("network_error_tip", "Controlla la connessione di rete, quindi seleziona 'Riprova'.") ].iter().cloned().collect(); } From 19d1605d8c9b38d238b7ad4a18e32f7286027238 Mon Sep 17 00:00:00 2001 From: jxdv Date: Sun, 28 Jul 2024 09:13:15 +0000 Subject: [PATCH 012/541] Update trs (#8866) * update sk-tr * update cs-tr --- src/lang/cs.rs | 6 +++--- src/lang/sk.rs | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/lang/cs.rs b/src/lang/cs.rs index 574c38297427..4a4cc943c867 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -629,8 +629,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("enable-bot-desc", "1, Otevřete chat s @BotFather.\n2, Pošlete příkaz \"/newbot\". Po dokončení tohoto kroku obdržíte token.\n3, Spusťte chat s nově vytvořeným botem. Pro jeho aktivaci odešlete zprávu začínající lomítkem vpřed (\"/\"), například \"/hello\".\n"), ("cancel-2fa-confirm-tip", "Jste si jisti, že chcete zrušit 2FA?"), ("cancel-bot-confirm-tip", "Jste si jisti, že chcete zrušit bota Telegramu?"), - ("About RustDesk", ""), - ("Send clipboard keystrokes", ""), - ("network_error_tip", ""), + ("About RustDesk", "O RustDesk"), + ("Send clipboard keystrokes", "Odesílat stisky kláves schránky"), + ("network_error_tip", "Zkontrolujte prosím připojení k síti a klikněte na tlačítko Opakovat."), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 9eaed3875b30..24b4d2e0998e 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -629,8 +629,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("enable-bot-desc", "1, Otvorte chat s @BotFather.\n2, Odošlite príkaz \"/newbot\". Po dokončení tohto kroku dostanete token.\n3, Spustite chat s novo vytvoreným botom. Odošlite správu začínajúcu lomítkom vpred (\"/\"), napríklad \"/hello\", aby ste ho aktivovali.\n"), ("cancel-2fa-confirm-tip", "Ste si istí, že chcete zrušiť službu 2FA?"), ("cancel-bot-confirm-tip", "Ste si istí, že chcete zrušiť bota Telegramu?"), - ("About RustDesk", ""), - ("Send clipboard keystrokes", ""), - ("network_error_tip", ""), + ("About RustDesk", "O RustDesk"), + ("Send clipboard keystrokes", "Odoslať stlačenia klávesov zo schránky"), + ("network_error_tip", "Skontrolujte svoje sieťové pripojenie a potom kliknite na tlačidlo Opakovať."), ].iter().cloned().collect(); } From 8c91e5c5ca169d3018b96fb729ba64ec972e9ccd Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Sun, 28 Jul 2024 17:26:18 +0800 Subject: [PATCH 013/541] refact: update crate tfc (#8867) Signed-off-by: fufesou --- Cargo.lock | 6 +++--- libs/enigo/Cargo.toml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 936d12b01325..ee757b7f6403 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6593,11 +6593,11 @@ dependencies = [ [[package]] name = "tfc" -version = "0.6.1" -source = "git+https://github.com/rustdesk-org/The-Fat-Controller#9dd86151525fd010dc93f6bc9b6aedd1a75cc342" +version = "0.7.0" +source = "git+https://github.com/rustdesk-org/The-Fat-Controller?branch=history/rebase_upstream_20240722#de9c8ba480f166a9fc90aaa47bb0e84b443ea9c6" dependencies = [ "anyhow", - "core-graphics 0.22.3", + "core-graphics 0.23.2", "unicode-segmentation", "winapi 0.3.9", "x11 2.19.0", diff --git a/libs/enigo/Cargo.toml b/libs/enigo/Cargo.toml index 1549209e13c0..a5b6d5622eda 100644 --- a/libs/enigo/Cargo.toml +++ b/libs/enigo/Cargo.toml @@ -23,7 +23,7 @@ serde = { version = "1.0", optional = true } serde_derive = { version = "1.0", optional = true } log = "0.4" rdev = { git = "https://github.com/rustdesk-org/rdev" } -tfc = { git = "https://github.com/rustdesk-org/The-Fat-Controller" } +tfc = { git = "https://github.com/rustdesk-org/The-Fat-Controller", branch = "history/rebase_upstream_20240722" } hbb_common = { path = "../hbb_common" } [features] From 541d9c6b86fb2cb649659106eacb982c7c61357f Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Sun, 28 Jul 2024 17:26:54 +0800 Subject: [PATCH 014/541] feat: clipboard, multi formats (#8733) Signed-off-by: fufesou --- .github/workflows/flutter-build.yml | 2 +- .github/workflows/playground.yml | 2 +- Cargo.lock | 322 ++-------------- Cargo.toml | 4 +- appimage/AppImageBuilder-aarch64.yml | 2 +- appimage/AppImageBuilder-x86_64.yml | 2 +- flutter/pubspec.yaml | 2 +- libs/clipboard/src/platform/mod.rs | 3 + libs/hbb_common/protos/message.proto | 14 + libs/portable/Cargo.toml | 2 +- res/PKGBUILD | 2 +- res/rpm-flutter-suse.spec | 2 +- res/rpm-flutter.spec | 2 +- res/rpm.spec | 2 +- src/client.rs | 177 ++++++--- src/client/io_loop.rs | 46 ++- src/clipboard.rs | 546 +++++++++++++++------------ src/flutter.rs | 13 + src/server/clipboard_service.rs | 98 +++-- src/server/connection.rs | 22 +- 20 files changed, 617 insertions(+), 648 deletions(-) diff --git a/.github/workflows/flutter-build.yml b/.github/workflows/flutter-build.yml index e4bf8eaf6a59..0e2b450639cf 100644 --- a/.github/workflows/flutter-build.yml +++ b/.github/workflows/flutter-build.yml @@ -33,7 +33,7 @@ env: VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite" # vcpkg version: 2024.07.12 VCPKG_COMMIT_ID: "1de2026f28ead93ff1773e6e680387643e914ea1" - VERSION: "1.2.7" + VERSION: "1.3.0" NDK_VERSION: "r26d" #signing keys env variable checks ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}" diff --git a/.github/workflows/playground.yml b/.github/workflows/playground.yml index d788858ab4c5..3fdcc4cfee85 100644 --- a/.github/workflows/playground.yml +++ b/.github/workflows/playground.yml @@ -18,7 +18,7 @@ env: VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite" # vcpkg version: 2024.06.15 VCPKG_COMMIT_ID: "f7423ee180c4b7f40d43402c2feb3859161ef625" - VERSION: "1.2.7" + VERSION: "1.3.0" NDK_VERSION: "r26d" #signing keys env variable checks ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}" diff --git a/Cargo.lock b/Cargo.lock index ee757b7f6403..43b8ed497ceb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -224,7 +224,7 @@ checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" [[package]] name = "arboard" version = "3.4.0" -source = "git+https://github.com/rustdesk-org/arboard#27b4e503caa70ec6306e5270461429f2cf907ad6" +source = "git+https://github.com/rustdesk-org/arboard#75166f255bf2fd6c662269029f5130b11d024f46" dependencies = [ "clipboard-win", "core-graphics 0.23.2", @@ -234,24 +234,11 @@ dependencies = [ "objc2-app-kit", "objc2-foundation", "parking_lot", - "resvg", "windows-sys 0.48.0", "wl-clipboard-rs", "x11rb 0.13.1", ] -[[package]] -name = "arrayref" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545" - -[[package]] -name = "arrayvec" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" - [[package]] name = "async-broadcast" version = "0.5.1" @@ -987,11 +974,14 @@ dependencies = [ [[package]] name = "clipboard-master" version = "4.0.0-beta.6" -source = "git+https://github.com/rustdesk-org/clipboard-master#5268c7b3d7728699566ad863da0911f249706f8c" +source = "git+https://github.com/rustdesk-org/clipboard-master#4fb62e5b62fb6350d82b571ec7ba94b3cd466695" dependencies = [ "objc", "objc-foundation", "objc_id", + "wayland-client", + "wayland-protocols", + "wayland-protocols-wlr", "windows-win", "wl-clipboard-rs", "x11-clipboard 0.9.2", @@ -1000,9 +990,9 @@ dependencies = [ [[package]] name = "clipboard-win" -version = "5.3.1" +version = "5.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79f4473f5144e20d9aceaf2972478f06ddf687831eafeeb434fbaf0acc4144ad" +checksum = "15efe7a882b08f34e38556b14f2fb3daa98769d06c7f0c1b076dfd0d983bc892" dependencies = [ "error-code", ] @@ -1526,12 +1516,6 @@ dependencies = [ "dasp_sample", ] -[[package]] -name = "data-url" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c297a1c74b71ae29df00c3e22dd9534821d60eb9af5a0192823fa2acea70c2a" - [[package]] name = "dbus" version = "0.9.7" @@ -2081,12 +2065,6 @@ dependencies = [ "thiserror", ] -[[package]] -name = "float-cmp" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" - [[package]] name = "flume" version = "0.11.0" @@ -2143,29 +2121,6 @@ dependencies = [ "libm", ] -[[package]] -name = "fontconfig-parser" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a595cb550439a117696039dfc69830492058211b771a2a165379f2a1a53d84d" -dependencies = [ - "roxmltree 0.19.0", -] - -[[package]] -name = "fontdb" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e32eac81c1135c1df01d4e6d4233c47ba11f6a6d07f33e0bba09d18797077770" -dependencies = [ - "fontconfig-parser", - "log", - "memmap2", - "slotmap", - "tinyvec", - "ttf-parser", -] - [[package]] name = "foreign-types" version = "0.3.2" @@ -3213,12 +3168,6 @@ dependencies = [ "tiff", ] -[[package]] -name = "imagesize" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "029d73f573d8e8d63e6d5020011d3255b28c3ba85d6cf870a07184ed23de9284" - [[package]] name = "impersonate_system" version = "0.1.0" @@ -3462,16 +3411,6 @@ dependencies = [ "unicode-segmentation", ] -[[package]] -name = "kurbo" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e5aa9f0f96a938266bdb12928a67169e8d22c6a786fda8ed984b85e6ba93c3c" -dependencies = [ - "arrayvec", - "smallvec", -] - [[package]] name = "lazy_static" version = "1.5.0" @@ -3777,15 +3716,6 @@ version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" -[[package]] -name = "memmap2" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe751422e4a8caa417e13c3ea66452215d7d63e19e604f4980461212f3ae1322" -dependencies = [ - "libc", -] - [[package]] name = "memoffset" version = "0.6.5" @@ -4732,15 +4662,9 @@ version = "0.7.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "234f71a15de2288bcb7e3b6515828d22af7ec8598ee6d24c3b526fa0a80b67a0" dependencies = [ - "siphasher 0.2.3", + "siphasher", ] -[[package]] -name = "pico-args" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" - [[package]] name = "pin-project" version = "1.1.5" @@ -5065,6 +4989,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "quick-xml" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f24d770aeca0eacb81ac29dfbc55ebcc09312fdd1f8bbecdc7e4a84e000e3b4" +dependencies = [ + "memchr", +] + [[package]] name = "quote" version = "0.6.13" @@ -5414,31 +5347,6 @@ dependencies = [ "winreg 0.50.0", ] -[[package]] -name = "resvg" -version = "0.42.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "944d052815156ac8fa77eaac055220e95ba0b01fa8887108ca710c03805d9051" -dependencies = [ - "gif", - "jpeg-decoder", - "log", - "pico-args", - "rgb", - "svgtypes", - "tiny-skia", - "usvg", -] - -[[package]] -name = "rgb" -version = "0.8.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7439be6844e40133eda024efd85bf07f59d0dd2f59b10c00dd6cfb92cc5c741" -dependencies = [ - "bytemuck", -] - [[package]] name = "ring" version = "0.17.8" @@ -5463,18 +5371,6 @@ dependencies = [ "crossbeam-utils", ] -[[package]] -name = "roxmltree" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cd14fd5e3b777a7422cca79358c57a8f6e3a703d9ac187448d0daf220c2407f" - -[[package]] -name = "roxmltree" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" - [[package]] name = "rpassword" version = "2.1.0" @@ -5572,7 +5468,7 @@ dependencies = [ [[package]] name = "rustdesk" -version = "1.2.7" +version = "1.3.0" dependencies = [ "android-wakelock", "android_logger", @@ -5670,7 +5566,7 @@ dependencies = [ [[package]] name = "rustdesk-portable-packer" -version = "1.2.7" +version = "1.3.0" dependencies = [ "brotli", "dirs 5.0.1", @@ -5853,22 +5749,6 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" -[[package]] -name = "rustybuzz" -version = "0.14.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfb9cf8877777222e4a3bc7eb247e398b56baba500c38c1c46842431adc8b55c" -dependencies = [ - "bitflags 2.6.0", - "bytemuck", - "smallvec", - "ttf-parser", - "unicode-bidi-mirroring", - "unicode-ccc", - "unicode-properties", - "unicode-script", -] - [[package]] name = "ryu" version = "1.0.18" @@ -6159,27 +6039,12 @@ version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" -[[package]] -name = "simplecss" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a11be7c62927d9427e9f40f3444d5499d868648e2edbc4e2116de69e7ec0e89d" -dependencies = [ - "log", -] - [[package]] name = "siphasher" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b8de496cf83d4ed58b6be86c3a275b8602f6ffe98d3024a869e124147a9a3ac" -[[package]] -name = "siphasher" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" - [[package]] name = "slab" version = "0.4.9" @@ -6189,15 +6054,6 @@ dependencies = [ "autocfg 1.3.0", ] -[[package]] -name = "slotmap" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbff4acf519f630b3a3ddcfaea6c06b42174d9a44bc70c620e9ed1649d58b82a" -dependencies = [ - "version_check", -] - [[package]] name = "smallvec" version = "1.13.2" @@ -6268,15 +6124,6 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe895eb47f22e2ddd4dabc02bce419d2e643c8e3b585c78158b349195bc24d82" -[[package]] -name = "strict-num" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" -dependencies = [ - "float-cmp", -] - [[package]] name = "strsim" version = "0.8.0" @@ -6338,16 +6185,6 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" -[[package]] -name = "svgtypes" -version = "0.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fae3064df9b89391c9a76a0425a69d124aee9c5c28455204709e72c39868a43c" -dependencies = [ - "kurbo", - "siphasher 1.0.1", -] - [[package]] name = "syn" version = "0.15.44" @@ -6687,32 +6524,6 @@ dependencies = [ "time-core", ] -[[package]] -name = "tiny-skia" -version = "0.11.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab" -dependencies = [ - "arrayref", - "arrayvec", - "bytemuck", - "cfg-if 1.0.0", - "log", - "png", - "tiny-skia-path", -] - -[[package]] -name = "tiny-skia-path" -version = "0.11.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93" -dependencies = [ - "arrayref", - "bytemuck", - "strict-num", -] - [[package]] name = "tinyvec" version = "1.6.1" @@ -7004,12 +6815,6 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" -[[package]] -name = "ttf-parser" -version = "0.21.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c591d83f69777866b9126b24c6dd9a18351f177e49d625920d19f989fd31cf8" - [[package]] name = "typenum" version = "1.17.0" @@ -7082,18 +6887,6 @@ version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" -[[package]] -name = "unicode-bidi-mirroring" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23cb788ffebc92c5948d0e997106233eeb1d8b9512f93f41651f52b6c5f5af86" - -[[package]] -name = "unicode-ccc" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1df77b101bcc4ea3d78dafc5ad7e4f58ceffe0b2b16bf446aeb50b6cb4157656" - [[package]] name = "unicode-ident" version = "1.0.12" @@ -7109,30 +6902,12 @@ dependencies = [ "tinyvec", ] -[[package]] -name = "unicode-properties" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4259d9d4425d9f0661581b804cb85fe66a4c631cadd8f490d1c13a35d5d9291" - -[[package]] -name = "unicode-script" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad8d71f5726e5f285a935e9fe8edfd53f0491eb6e9a5774097fdabee7cd8c9cd" - [[package]] name = "unicode-segmentation" version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" -[[package]] -name = "unicode-vo" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1d386ff53b415b7fe27b50bb44679e2cc4660272694b7b6f3326d8480823a94" - [[package]] name = "unicode-width" version = "0.1.13" @@ -7195,33 +6970,6 @@ dependencies = [ "log", ] -[[package]] -name = "usvg" -version = "0.42.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b84ea542ae85c715f07b082438a4231c3760539d902e11d093847a0b22963032" -dependencies = [ - "base64 0.22.1", - "data-url", - "flate2", - "fontdb", - "imagesize", - "kurbo", - "log", - "pico-args", - "roxmltree 0.20.0", - "rustybuzz", - "simplecss", - "siphasher 1.0.1", - "strict-num", - "svgtypes", - "tiny-skia-path", - "unicode-bidi", - "unicode-script", - "unicode-vo", - "xmlwriter", -] - [[package]] name = "utf16string" version = "0.2.0" @@ -7414,9 +7162,9 @@ checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" [[package]] name = "wayland-backend" -version = "0.3.4" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34e9e6b6d4a2bb4e7e69433e0b35c7923b95d4dc8503a84d25ec917a4bbfdf07" +checksum = "f90e11ce2ca99c97b940ee83edbae9da2d56a08f9ea8158550fd77fa31722993" dependencies = [ "cc", "downcast-rs", @@ -7428,9 +7176,9 @@ dependencies = [ [[package]] name = "wayland-client" -version = "0.31.3" +version = "0.31.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e63801c85358a431f986cffa74ba9599ff571fc5774ac113ed3b490c19a1133" +checksum = "7e321577a0a165911bdcfb39cf029302479d7527b517ee58ab0f6ad09edf0943" dependencies = [ "bitflags 2.6.0", "rustix 0.38.34", @@ -7440,9 +7188,9 @@ dependencies = [ [[package]] name = "wayland-protocols" -version = "0.32.1" +version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83d0f1056570486e26a3773ec633885124d79ae03827de05ba6c85f79904026c" +checksum = "62989625a776e827cc0f15d41444a3cea5205b963c3a25be48ae1b52d6b4daaa" dependencies = [ "bitflags 2.6.0", "wayland-backend", @@ -7452,9 +7200,9 @@ dependencies = [ [[package]] name = "wayland-protocols-wlr" -version = "0.3.1" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7dab47671043d9f5397035975fe1cac639e5bca5cc0b3c32d09f01612e34d24" +checksum = "fd993de54a40a40fbe5601d9f1fbcaef0aebcc5fda447d7dc8f6dcbaae4f8953" dependencies = [ "bitflags 2.6.0", "wayland-backend", @@ -7465,20 +7213,20 @@ dependencies = [ [[package]] name = "wayland-scanner" -version = "0.31.2" +version = "0.31.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67da50b9f80159dec0ea4c11c13e24ef9e7574bd6ce24b01860a175010cea565" +checksum = "d7b56f89937f1cf2ee1f1259cf2936a17a1f45d8f0aa1019fae6d470d304cfa6" dependencies = [ "proc-macro2 1.0.86", - "quick-xml 0.31.0", + "quick-xml 0.34.0", "quote 1.0.36", ] [[package]] name = "wayland-sys" -version = "0.31.2" +version = "0.31.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "105b1842da6554f91526c14a2a2172897b7f745a805d62af4ce698706be79c12" +checksum = "43676fe2daf68754ecf1d72026e4e6c15483198b5d24e888b74d3f22f887a148" dependencies = [ "dlib", "log", @@ -8220,12 +7968,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "xmlwriter" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" - [[package]] name = "zbus" version = "3.15.2" diff --git a/Cargo.toml b/Cargo.toml index 85b2b540121d..c1a841358693 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rustdesk" -version = "1.2.7" +version = "1.3.0" authors = ["rustdesk "] edition = "2021" build= "build.rs" @@ -90,7 +90,7 @@ enigo = { path = "libs/enigo", features = [ "with_serde" ] } clipboard = { path = "libs/clipboard" } ctrlc = "3.2" # arboard = { version = "3.4.0", features = ["wayland-data-control"] } -arboard = { git = "https://github.com/rustdesk-org/arboard", features = ["wayland-data-control", "image-data"] } +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" diff --git a/appimage/AppImageBuilder-aarch64.yml b/appimage/AppImageBuilder-aarch64.yml index 411d7bb571d9..c64966f28192 100644 --- a/appimage/AppImageBuilder-aarch64.yml +++ b/appimage/AppImageBuilder-aarch64.yml @@ -18,7 +18,7 @@ AppDir: id: rustdesk name: rustdesk icon: rustdesk - version: 1.2.7 + version: 1.3.0 exec: usr/lib/rustdesk/rustdesk exec_args: $@ apt: diff --git a/appimage/AppImageBuilder-x86_64.yml b/appimage/AppImageBuilder-x86_64.yml index 9c6860dd43e6..3c0479d962f0 100644 --- a/appimage/AppImageBuilder-x86_64.yml +++ b/appimage/AppImageBuilder-x86_64.yml @@ -18,7 +18,7 @@ AppDir: id: rustdesk name: rustdesk icon: rustdesk - version: 1.2.7 + version: 1.3.0 exec: usr/lib/rustdesk/rustdesk exec_args: $@ apt: diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index c6e1e61cc5fa..a0a24d54d7e6 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # 1.1.9-1 works for android, but for ios it becomes 1.1.91, need to set it to 1.1.9-a.1 for iOS, will get 1.1.9.1, but iOS store not allow 4 numbers -version: 1.2.7+46 +version: 1.3.0+46 environment: sdk: '^3.1.0' diff --git a/libs/clipboard/src/platform/mod.rs b/libs/clipboard/src/platform/mod.rs index 2be4ce809da0..5db271129734 100644 --- a/libs/clipboard/src/platform/mod.rs +++ b/libs/clipboard/src/platform/mod.rs @@ -1,3 +1,4 @@ +#[cfg(any(target_os = "linux", target_os = "macos"))] use crate::{CliprdrError, CliprdrServiceContext}; #[cfg(target_os = "windows")] @@ -63,8 +64,10 @@ pub fn create_cliprdr_context( return Ok(Box::new(DummyCliprdrContext {}) as Box<_>); } +#[cfg(any(target_os = "linux", target_os = "macos"))] struct DummyCliprdrContext {} +#[cfg(any(target_os = "linux", target_os = "macos"))] impl CliprdrServiceContext for DummyCliprdrContext { fn set_is_stopped(&mut self) -> Result<(), CliprdrError> { Ok(()) diff --git a/libs/hbb_common/protos/message.proto b/libs/hbb_common/protos/message.proto index f346a72288c0..d7a8cf0a7cd6 100644 --- a/libs/hbb_common/protos/message.proto +++ b/libs/hbb_common/protos/message.proto @@ -81,6 +81,7 @@ message LoginRequest { uint64 session_id = 10; string version = 11; OSLogin os_login = 12; + string my_platform = 13; } message Auth2FA { @@ -315,13 +316,25 @@ message Hash { string challenge = 2; } +enum ClipboardFormat { + Text = 0; + Rtf = 1; + Html = 2; + ImageRgba = 21; + ImagePng = 22; + ImageSvg = 23; +} + message Clipboard { bool compress = 1; bytes content = 2; int32 width = 3; int32 height = 4; + ClipboardFormat format = 5; } +message MultiClipboards { repeated Clipboard clipboards = 1; } + enum FileType { Dir = 0; DirLink = 2; @@ -816,5 +829,6 @@ message Message { PeerInfo peer_info = 25; PointerDeviceEvent pointer_device_event = 26; Auth2FA auth_2fa = 27; + MultiClipboards multi_clipboards = 28; } } diff --git a/libs/portable/Cargo.toml b/libs/portable/Cargo.toml index e39212bf28ba..ce1c10c09e40 100644 --- a/libs/portable/Cargo.toml +++ b/libs/portable/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rustdesk-portable-packer" -version = "1.2.7" +version = "1.3.0" edition = "2021" description = "RustDesk Remote Desktop" diff --git a/res/PKGBUILD b/res/PKGBUILD index 6f1b4e6801f1..94ccce6937d0 100644 --- a/res/PKGBUILD +++ b/res/PKGBUILD @@ -1,5 +1,5 @@ pkgname=rustdesk -pkgver=1.2.7 +pkgver=1.3.0 pkgrel=0 epoch= pkgdesc="" diff --git a/res/rpm-flutter-suse.spec b/res/rpm-flutter-suse.spec index c4fe69e67788..053099c07fa2 100644 --- a/res/rpm-flutter-suse.spec +++ b/res/rpm-flutter-suse.spec @@ -1,5 +1,5 @@ Name: rustdesk -Version: 1.2.7 +Version: 1.3.0 Release: 0 Summary: RPM package License: GPL-3.0 diff --git a/res/rpm-flutter.spec b/res/rpm-flutter.spec index 90e45af9cbf4..f962a2ed1f6a 100644 --- a/res/rpm-flutter.spec +++ b/res/rpm-flutter.spec @@ -1,5 +1,5 @@ Name: rustdesk -Version: 1.2.7 +Version: 1.3.0 Release: 0 Summary: RPM package License: GPL-3.0 diff --git a/res/rpm.spec b/res/rpm.spec index a6d6a956a670..633c2a220a78 100644 --- a/res/rpm.spec +++ b/res/rpm.spec @@ -1,5 +1,5 @@ Name: rustdesk -Version: 1.2.7 +Version: 1.3.0 Release: 0 Summary: RPM package License: GPL-3.0 diff --git a/src/client.rs b/src/client.rs index e5823e187d13..6fc8ce1a6af8 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,14 +1,7 @@ -use std::{ - collections::HashMap, - ffi::c_void, - net::SocketAddr, - ops::Deref, - str::FromStr, - sync::{mpsc, Arc, Mutex, RwLock}, -}; - use async_trait::async_trait; use bytes::Bytes; +#[cfg(not(any(target_os = "android", target_os = "ios")))] +use clipboard_master::{CallbackResult, ClipboardHandler}; #[cfg(not(any(target_os = "android", target_os = "linux")))] use cpal::{ traits::{DeviceTrait, HostTrait, StreamTrait}, @@ -19,6 +12,18 @@ use magnum_opus::{Channels::*, Decoder as AudioDecoder}; #[cfg(not(any(target_os = "android", target_os = "linux")))] use ringbuf::{ring_buffer::RbBase, Rb}; use sha2::{Digest, Sha256}; +use std::{ + collections::HashMap, + ffi::c_void, + io, + net::SocketAddr, + ops::Deref, + str::FromStr, + sync::{ + mpsc::{self, RecvTimeoutError, Sender}, + Arc, Mutex, RwLock, + }, +}; use uuid::Uuid; pub use file_trait::FileManager; @@ -65,7 +70,7 @@ use crate::{ }; #[cfg(not(any(target_os = "android", target_os = "ios")))] -use crate::clipboard::{check_clipboard, CLIPBOARD_INTERVAL}; +use crate::clipboard::{check_clipboard, ClipboardSide, CLIPBOARD_INTERVAL}; #[cfg(not(feature = "flutter"))] #[cfg(not(any(target_os = "android", target_os = "ios")))] use crate::ui_session_interface::SessionPermissionConfig; @@ -136,18 +141,11 @@ lazy_static::lazy_static! { #[cfg(not(any(target_os = "android", target_os = "ios")))] lazy_static::lazy_static! { static ref ENIGO: Arc> = Arc::new(Mutex::new(enigo::Enigo::new())); - static ref OLD_CLIPBOARD_DATA: Arc> = Default::default(); static ref TEXT_CLIPBOARD_STATE: Arc> = Arc::new(Mutex::new(TextClipboardState::new())); } const PUBLIC_SERVER: &str = "public"; -#[inline] -#[cfg(not(any(target_os = "android", target_os = "ios")))] -pub fn get_old_clipboard_text() -> Arc> { - OLD_CLIPBOARD_DATA.clone() -} - #[cfg(not(any(target_os = "android", target_os = "ios")))] pub fn get_key_state(key: enigo::Key) -> bool { use enigo::KeyboardControllable; @@ -714,6 +712,13 @@ impl Client { #[cfg(not(any(target_os = "android", target_os = "ios")))] fn try_stop_clipboard() { + // There's a bug here. + // If session is closed by the peer, `has_sessions_running()` will always return true. + // It's better to check if the active session number. + // But it's not a problem, because the clipboard thread does not consume CPU. + // + // If we want to fix it, we can add a flag to indicate if session is active. + // But I think it's not necessary to introduce complexity at this point. #[cfg(feature = "flutter")] if crate::flutter::sessions::has_sessions_running(ConnType::DEFAULT_CONN) { return; @@ -727,18 +732,44 @@ impl Client { // // If clipboard update is detected, the text will be sent to all sessions by `send_text_clipboard_msg`. #[cfg(not(any(target_os = "android", target_os = "ios")))] - fn try_start_clipboard(_ctx: Option) -> Option> { + fn try_start_clipboard( + _client_clip_ctx: Option, + ) -> Option> { let mut clipboard_lock = TEXT_CLIPBOARD_STATE.lock().unwrap(); if clipboard_lock.running { return None; } + + let (tx_cb_result, rx_cb_result) = mpsc::channel(); + let handler = ClientClipboardHandler { + ctx: None, + tx_cb_result, + #[cfg(not(feature = "flutter"))] + client_clip_ctx: _client_clip_ctx, + }; + + let (tx_start_res, rx_start_res) = mpsc::channel(); + let h = crate::clipboard::start_clipbard_master_thread(handler, tx_start_res); + let shutdown = match rx_start_res.recv() { + Ok((Some(s), _)) => s, + Ok((None, err)) => { + log::error!("{}", err); + return None; + } + Err(e) => { + log::error!("Failed to create clipboard listener: {}", e); + return None; + } + }; + clipboard_lock.running = true; - let (tx, rx) = unbounded_channel(); + + let (tx_started, rx_started) = unbounded_channel(); log::info!("Start text clipboard loop"); std::thread::spawn(move || { let mut is_sent = false; - let mut ctx = None; + loop { if !TEXT_CLIPBOARD_STATE.lock().unwrap().running { break; @@ -748,39 +779,31 @@ impl Client { continue; } - if let Some(msg) = check_clipboard(&mut ctx, Some(OLD_CLIPBOARD_DATA.clone())) { - #[cfg(feature = "flutter")] - crate::flutter::send_text_clipboard_msg(msg); - #[cfg(not(feature = "flutter"))] - if let Some(ctx) = &_ctx { - if ctx.cfg.is_text_clipboard_required() { - let _ = ctx.tx.send(Data::Message(msg)); - } - } - } - if !is_sent { is_sent = true; - tx.send(()).ok(); + tx_started.send(()).ok(); } - std::thread::sleep(Duration::from_millis(CLIPBOARD_INTERVAL)); + match rx_cb_result.recv_timeout(Duration::from_millis(CLIPBOARD_INTERVAL)) { + Ok(CallbackResult::Stop) => { + log::debug!("Clipboard listener stopped"); + break; + } + Ok(CallbackResult::StopWithError(err)) => { + log::error!("Clipboard listener stopped with error: {}", err); + break; + } + Err(RecvTimeoutError::Timeout) => {} + _ => {} + } } log::info!("Stop text clipboard loop"); + shutdown.signal(); + h.join().ok(); + TEXT_CLIPBOARD_STATE.lock().unwrap().running = false; }); - Some(rx) - } - - #[inline] - #[cfg(not(any(target_os = "android", target_os = "ios")))] - fn get_current_clipboard_msg() -> Option { - let data = &*OLD_CLIPBOARD_DATA.lock().unwrap(); - if data.is_empty() { - None - } else { - Some(data.create_msg()) - } + Some(rx_started) } } @@ -794,6 +817,65 @@ impl TextClipboardState { } } +#[cfg(not(any(target_os = "android", target_os = "ios")))] +struct ClientClipboardHandler { + ctx: Option, + tx_cb_result: Sender, + #[cfg(not(feature = "flutter"))] + client_clip_ctx: Option, +} + +#[cfg(not(any(target_os = "android", target_os = "ios")))] +impl ClientClipboardHandler { + #[inline] + #[cfg(feature = "flutter")] + fn send_msg(&self, msg: Message) { + crate::flutter::send_text_clipboard_msg(msg); + } + + #[cfg(not(feature = "flutter"))] + fn send_msg(&self, msg: Message) { + if let Some(ctx) = &self.client_clip_ctx { + if ctx.cfg.is_text_clipboard_required() { + if let Some(pi) = ctx.cfg.lc.read().unwrap().peer_info.as_ref() { + if let Some(message::Union::MultiClipboards(multi_clipboards)) = &msg.union { + if let Some(msg_out) = crate::clipboard::get_msg_if_not_support_multi_clip( + &pi.version, + &pi.platform, + multi_clipboards, + ) { + let _ = ctx.tx.send(Data::Message(msg_out)); + return; + } + } + } + let _ = ctx.tx.send(Data::Message(msg)); + } + } + } +} + +#[cfg(not(any(target_os = "android", target_os = "ios")))] +impl ClipboardHandler for ClientClipboardHandler { + fn on_clipboard_change(&mut self) -> CallbackResult { + if TEXT_CLIPBOARD_STATE.lock().unwrap().running + && TEXT_CLIPBOARD_STATE.lock().unwrap().is_required + { + if let Some(msg) = check_clipboard(&mut self.ctx, ClipboardSide::Client, false) { + self.send_msg(msg); + } + } + CallbackResult::Next + } + + fn on_clipboard_error(&mut self, error: io::Error) -> CallbackResult { + self.tx_cb_result + .send(CallbackResult::StopWithError(error)) + .ok(); + CallbackResult::Next + } +} + /// Audio handler for the [`Client`]. #[derive(Default)] pub struct AudioHandler { @@ -2031,11 +2113,16 @@ impl LoginConfigHandler { if display_name.is_empty() { display_name = crate::username(); } + #[cfg(not(target_os = "android"))] + let my_platform = whoami::platform().to_string(); + #[cfg(target_os = "android")] + let my_platform = "Android".into(); let mut lr = LoginRequest { username: pure_id, password: password.into(), my_id, my_name: display_name, + my_platform, option: self.get_option_message(true).into(), session_id: self.session_id, version: crate::VERSION.to_string(), diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index 1280fb80120b..a182a5fe0224 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -7,11 +7,21 @@ use std::{ }, }; +#[cfg(not(any(target_os = "android", target_os = "ios")))] +use crate::clipboard::{update_clipboard, ClipboardSide, CLIPBOARD_INTERVAL}; +#[cfg(not(any(target_os = "ios")))] +use crate::{audio_service, ConnInner, CLIENT_SERVER}; +use crate::{ + client::{ + self, new_voice_call_request, Client, Data, Interface, MediaData, MediaSender, + QualityStatus, MILLI1, SEC30, + }, + common::get_default_sound_input, + ui_session_interface::{InvokeUiSession, Session}, +}; #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] use clipboard::ContextSend; use crossbeam_queue::ArrayQueue; -#[cfg(not(any(target_os = "android", target_os = "ios")))] -use hbb_common::sleep; #[cfg(not(target_os = "ios"))] use hbb_common::tokio::sync::mpsc::error::TryRecvError; use hbb_common::{ @@ -37,17 +47,6 @@ use hbb_common::{ use hbb_common::{tokio::sync::Mutex as TokioMutex, ResultType}; use scrap::CodecFormat; -use crate::client::{ - self, new_voice_call_request, Client, MediaData, MediaSender, QualityStatus, MILLI1, SEC30, -}; -#[cfg(not(any(target_os = "android", target_os = "ios")))] -use crate::clipboard::{update_clipboard, CLIPBOARD_INTERVAL}; -use crate::common::get_default_sound_input; -use crate::ui_session_interface::{InvokeUiSession, Session}; -#[cfg(not(any(target_os = "ios")))] -use crate::{audio_service, ConnInner, CLIENT_SERVER}; -use crate::{client::Data, client::Interface}; - pub struct Remote { handler: Session, video_queue_map: Arc>>>, @@ -173,8 +172,7 @@ impl Remote { crate::rustdesk_interval(time::interval(Duration::new(1, 0))); let mut fps_instant = Instant::now(); - let _keep_it = - client::hc_connection(feedback, rendezvous_server, token).await; + let _keep_it = client::hc_connection(feedback, rendezvous_server, token).await; loop { tokio::select! { @@ -1123,6 +1121,8 @@ impl Remote { } } Some(login_response::Union::PeerInfo(pi)) => { + let peer_version = pi.version.clone(); + let peer_platform = pi.platform.clone(); self.handler.handle_peer_info(pi); self.check_clipboard_file_context(); if !(self.handler.is_file_transfer() || self.handler.is_port_forward()) { @@ -1144,12 +1144,14 @@ impl Remote { } #[cfg(not(any(target_os = "android", target_os = "ios")))] - if let Some(msg_out) = Client::get_current_clipboard_msg() { + if let Some(msg_out) = crate::clipboard::get_current_clipboard_msg( + &peer_version, + &peer_platform, + crate::clipboard::ClipboardSide::Client, + ) { let sender = self.sender.clone(); let permission_config = self.handler.get_permission_config(); tokio::spawn(async move { - // due to clipboard service interval time - sleep(CLIPBOARD_INTERVAL as f32 / 1_000.).await; if permission_config.is_text_clipboard_required() { sender.send(Data::Message(msg_out)).ok(); } @@ -1185,7 +1187,7 @@ impl Remote { Some(message::Union::Clipboard(cb)) => { if !self.handler.lc.read().unwrap().disable_clipboard.v { #[cfg(not(any(target_os = "android", target_os = "ios")))] - update_clipboard(cb, Some(crate::client::get_old_clipboard_text())); + update_clipboard(vec![cb], ClipboardSide::Client); #[cfg(any(target_os = "android", target_os = "ios"))] { let content = if cb.compress { @@ -1199,6 +1201,12 @@ impl Remote { } } } + Some(message::Union::MultiClipboards(_mcb)) => { + if !self.handler.lc.read().unwrap().disable_clipboard.v { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + update_clipboard(_mcb.clipboards, ClipboardSide::Client); + } + } #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] Some(message::Union::Cliprdr(clip)) => { self.handle_cliprdr_msg(clip); diff --git a/src/clipboard.rs b/src/clipboard.rs index 0db9f59c1440..fe30189caaaa 100644 --- a/src/clipboard.rs +++ b/src/clipboard.rs @@ -1,26 +1,35 @@ -use std::sync::{ - atomic::{AtomicU64, Ordering}, - Arc, Mutex, -}; - -use clipboard_master::{CallbackResult, ClipboardHandler, Master, Shutdown}; -use hbb_common::{ - allow_err, - compress::{compress as compress_func, decompress}, - log, - message_proto::*, - ResultType, +use arboard::{ClipboardData, ClipboardFormat}; +use clipboard_master::{ClipboardHandler, Master, Shutdown}; +use hbb_common::{log, message_proto::*, ResultType}; +use std::{ + sync::{mpsc::Sender, Arc, Mutex}, + thread, + thread::JoinHandle, + time::Duration, }; pub const CLIPBOARD_NAME: &'static str = "clipboard"; pub const CLIPBOARD_INTERVAL: u64 = 333; -const FAKE_SVG_WIDTH: usize = 999999; + +// This format is used to store the flag in the clipboard. +const RUSTDESK_CLIPBOARD_OWNER_FORMAT: &'static str = "dyn.com.rustdesk.owner"; lazy_static::lazy_static! { - pub static ref CONTENT: Arc> = Default::default(); static ref ARBOARD_MTX: Arc> = Arc::new(Mutex::new(())); + // cache the clipboard msg + static ref LAST_MULTI_CLIPBOARDS: Arc> = Arc::new(Mutex::new(MultiClipboards::new())); } +const SUPPORTED_FORMATS: &[ClipboardFormat] = &[ + ClipboardFormat::Text, + ClipboardFormat::Html, + ClipboardFormat::Rtf, + ClipboardFormat::ImageRgba, + ClipboardFormat::ImagePng, + ClipboardFormat::ImageSvg, + ClipboardFormat::Special(RUSTDESK_CLIPBOARD_OWNER_FORMAT), +]; + #[cfg(all(target_os = "linux", feature = "unix-file-copy-paste"))] static X11_CLIPBOARD: once_cell::sync::OnceCell = once_cell::sync::OnceCell::new(); @@ -61,7 +70,7 @@ fn parse_plain_uri_list(v: Vec) -> Result { #[cfg(all(target_os = "linux", feature = "unix-file-copy-paste"))] impl ClipboardContext { - pub fn new(_listen: bool) -> Result { + pub fn new() -> Result { let clipboard = get_clipboard()?; let string_getter = clipboard .getter @@ -128,56 +137,42 @@ impl ClipboardContext { pub fn check_clipboard( ctx: &mut Option, - old: Option>>, + side: ClipboardSide, + force: bool, ) -> Option { if ctx.is_none() { - *ctx = ClipboardContext::new(true).ok(); + *ctx = ClipboardContext::new().ok(); } let ctx2 = ctx.as_mut()?; - let side = if old.is_none() { "host" } else { "client" }; - let old = if let Some(old) = old { - old - } else { - CONTENT.clone() - }; - let content = ctx2.get(); + let content = ctx2.get(side, force); if let Ok(content) = content { if !content.is_empty() { - if matches!(content, ClipboardData::Text(_)) { - // Skip the text if the last content is image-svg/html - if ctx2.is_last_plain { - return None; - } - } - - let changed = content != *old.lock().unwrap(); - if changed { - log::info!("{} update found on {}", CLIPBOARD_NAME, side); - let msg = content.create_msg(); - *old.lock().unwrap() = content; - return Some(msg); - } + let mut msg = Message::new(); + let clipboards = proto::create_multi_clipboards(content); + msg.set_multi_clipboards(clipboards.clone()); + *LAST_MULTI_CLIPBOARDS.lock().unwrap() = clipboards; + return Some(msg); } } None } -fn update_clipboard_(clipboard: Clipboard, old: Option>>) { - let content = ClipboardData::from_msg(clipboard); - if content.is_empty() { +fn update_clipboard_(multi_clipboards: Vec, side: ClipboardSide) { + let mut to_update_data = proto::from_multi_clipbards(multi_clipboards); + if to_update_data.is_empty() { return; } - match ClipboardContext::new(false) { + match ClipboardContext::new() { Ok(mut ctx) => { - let side = if old.is_none() { "host" } else { "client" }; - let old = if let Some(old) = old { - old + to_update_data.push(ClipboardData::Special(( + RUSTDESK_CLIPBOARD_OWNER_FORMAT.to_owned(), + side.get_owner_data(), + ))); + if let Err(e) = ctx.set(&to_update_data) { + log::debug!("Failed to set clipboard: {}", e); } else { - CONTENT.clone() - }; - allow_err!(ctx.set(&content)); - *old.lock().unwrap() = content; - log::debug!("{} updated on {}", CLIPBOARD_NAME, side); + log::debug!("{} updated on {}", CLIPBOARD_NAME, side); + } } Err(err) => { log::error!("Failed to create clipboard context: {}", err); @@ -185,143 +180,21 @@ fn update_clipboard_(clipboard: Clipboard, old: Option> } } -pub fn update_clipboard(clipboard: Clipboard, old: Option>>) { +pub fn update_clipboard(multi_clipboards: Vec, side: ClipboardSide) { std::thread::spawn(move || { - update_clipboard_(clipboard, old); + update_clipboard_(multi_clipboards, side); }); } -#[derive(Clone)] -pub enum ClipboardData { - Text(String), - Image(arboard::ImageData<'static>, u64), - Empty, -} - -impl Default for ClipboardData { - fn default() -> Self { - ClipboardData::Empty - } -} - -impl ClipboardData { - fn image(image: arboard::ImageData<'static>) -> ClipboardData { - let hash = 0; - /* - use std::hash::{DefaultHasher, Hash, Hasher}; - let mut hasher = DefaultHasher::new(); - image.bytes.hash(&mut hasher); - let hash = hasher.finish(); - */ - ClipboardData::Image(image, hash) - } - - pub fn is_empty(&self) -> bool { - match self { - ClipboardData::Empty => true, - ClipboardData::Text(s) => s.is_empty(), - ClipboardData::Image(a, _) => a.bytes().is_empty(), - } - } - - fn from_msg(clipboard: Clipboard) -> Self { - let is_image = clipboard.width > 0; - let data = if clipboard.compress { - decompress(&clipboard.content) - } else { - clipboard.content.into() - }; - if is_image { - // We cannot use data.start_with(b" Message { - let mut msg = Message::new(); - - match self { - ClipboardData::Text(s) => { - let compressed = compress_func(s.as_bytes()); - let compress = compressed.len() < s.as_bytes().len(); - let content = if compress { - compressed - } else { - s.clone().into_bytes() - }; - msg.set_clipboard(Clipboard { - compress, - content: content.into(), - ..Default::default() - }); - } - ClipboardData::Image(a, _) => { - let compressed = compress_func(&a.bytes()); - let compress = compressed.len() < a.bytes().len(); - let content = if compress { - compressed - } else { - a.bytes().to_vec() - }; - let (w, h) = match a { - arboard::ImageData::Rgba(a) => (a.width, a.height), - arboard::ImageData::Svg(_) => (FAKE_SVG_WIDTH as _, 0 as _), - }; - msg.set_clipboard(Clipboard { - compress, - content: content.into(), - width: w as _, - height: h as _, - ..Default::default() - }); - } - _ => {} - } - msg - } -} - -impl PartialEq for ClipboardData { - fn eq(&self, other: &Self) -> bool { - match (self, other) { - (ClipboardData::Text(a), ClipboardData::Text(b)) => a == b, - (ClipboardData::Image(a, _), ClipboardData::Image(b, _)) => match (a, b) { - (arboard::ImageData::Rgba(a), arboard::ImageData::Rgba(b)) => { - a.width == b.width && a.height == b.height && a.bytes == b.bytes - } - (arboard::ImageData::Svg(a), arboard::ImageData::Svg(b)) => a == b, - _ => false, - }, - (ClipboardData::Empty, ClipboardData::Empty) => true, - _ => false, - } - } -} - #[cfg(not(any(all(target_os = "linux", feature = "unix-file-copy-paste"))))] pub struct ClipboardContext { inner: arboard::Clipboard, - counter: (Arc, u64), - shutdown: Option, - is_last_plain: bool, } #[cfg(not(any(all(target_os = "linux", feature = "unix-file-copy-paste"))))] #[allow(unreachable_code)] impl ClipboardContext { - pub fn new(listen: bool) -> ResultType { + pub fn new() -> ResultType { let board; #[cfg(not(target_os = "linux"))] { @@ -351,94 +224,275 @@ impl ClipboardContext { } } - // starting from 1 so that we can always get initial clipboard data no matter if change - let change_count: Arc = Arc::new(AtomicU64::new(1)); - let mut shutdown = None; - if listen { - struct Handler(Arc); - impl ClipboardHandler for Handler { - fn on_clipboard_change(&mut self) -> CallbackResult { - self.0.fetch_add(1, Ordering::SeqCst); - CallbackResult::Next - } + Ok(ClipboardContext { inner: board }) + } - fn on_clipboard_error(&mut self, error: std::io::Error) -> CallbackResult { - log::trace!("Error of clipboard listener: {}", error); - CallbackResult::Next + pub fn get(&mut self, side: ClipboardSide, force: bool) -> ResultType> { + let _lock = ARBOARD_MTX.lock().unwrap(); + let data = self.inner.get_formats(SUPPORTED_FORMATS)?; + if data.is_empty() { + return Ok(data); + } + if !force { + for c in data.iter() { + if let ClipboardData::Special((_, d)) = c { + if side.is_owner(d) { + return Ok(vec![]); + } } } - let change_count_cloned = change_count.clone(); - let (tx, rx) = std::sync::mpsc::channel(); - // https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getmessage#:~:text=The%20window%20must%20belong%20to%20the%20current%20thread. - std::thread::spawn(move || match Master::new(Handler(change_count_cloned)) { - Ok(mut master) => { - tx.send(master.shutdown_channel()).ok(); - log::debug!("Clipboard listener started"); - if let Err(err) = master.run() { - log::error!("Failed to run clipboard listener: {}", err); - } else { - log::debug!("Clipboard listener stopped"); - } + } + Ok(data + .into_iter() + .filter(|c| !matches!(c, ClipboardData::Special(_))) + .collect()) + } + + fn set(&mut self, data: &[ClipboardData]) -> ResultType<()> { + let _lock = ARBOARD_MTX.lock().unwrap(); + self.inner.set_formats(data)?; + Ok(()) + } +} + +pub fn is_support_multi_clipboard(peer_version: &str, peer_platform: &str) -> bool { + use hbb_common::get_version_number; + get_version_number(peer_version) >= get_version_number("1.3.0") + && !["", "Android", &whoami::Platform::Ios.to_string()].contains(&peer_platform) +} + +pub fn get_current_clipboard_msg( + peer_version: &str, + peer_platform: &str, + side: ClipboardSide, +) -> Option { + let mut multi_clipboards = LAST_MULTI_CLIPBOARDS.lock().unwrap(); + if multi_clipboards.clipboards.is_empty() { + let mut ctx = ClipboardContext::new().ok()?; + *multi_clipboards = proto::create_multi_clipboards(ctx.get(side, true).ok()?); + } + if multi_clipboards.clipboards.is_empty() { + return None; + } + + if is_support_multi_clipboard(peer_version, peer_platform) { + let mut msg = Message::new(); + msg.set_multi_clipboards(multi_clipboards.clone()); + Some(msg) + } else { + // Find the first text clipboard and send it. + multi_clipboards + .clipboards + .iter() + .find(|c| c.format.enum_value() == Ok(hbb_common::message_proto::ClipboardFormat::Text)) + .map(|c| { + let mut msg = Message::new(); + msg.set_clipboard(c.clone()); + msg + }) + } +} + +#[derive(PartialEq, Eq, Clone, Copy)] +pub enum ClipboardSide { + Host, + Client, +} + +impl ClipboardSide { + // 01: the clipboard is owned by the host + // 10: the clipboard is owned by the client + fn get_owner_data(&self) -> Vec { + match self { + ClipboardSide::Host => vec![0b01], + ClipboardSide::Client => vec![0b10], + } + } + + fn is_owner(&self, data: &[u8]) -> bool { + if data.len() == 0 { + return false; + } + data[0] & 0b11 != 0 + } +} + +impl std::fmt::Display for ClipboardSide { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ClipboardSide::Host => write!(f, "host"), + ClipboardSide::Client => write!(f, "client"), + } + } +} + +pub fn start_clipbard_master_thread( + handler: impl ClipboardHandler + Send + 'static, + tx_start_res: Sender<(Option, String)>, +) -> JoinHandle<()> { + // https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getmessage#:~:text=The%20window%20must%20belong%20to%20the%20current%20thread. + let h = std::thread::spawn(move || match Master::new(handler) { + Ok(mut master) => { + tx_start_res + .send((Some(master.shutdown_channel()), "".to_owned())) + .ok(); + log::debug!("Clipboard listener started"); + if let Err(err) = master.run() { + log::error!("Failed to run clipboard listener: {}", err); + } else { + log::debug!("Clipboard listener stopped"); + } + } + Err(err) => { + tx_start_res + .send(( + None, + format!("Failed to create clipboard listener: {}", err), + )) + .ok(); + } + }); + h +} + +pub use proto::get_msg_if_not_support_multi_clip; +mod proto { + use arboard::ClipboardData; + use hbb_common::{ + compress::{compress as compress_func, decompress}, + message_proto::{Clipboard, ClipboardFormat, Message, MultiClipboards}, + }; + + fn plain_to_proto(s: String, format: ClipboardFormat) -> Clipboard { + let compressed = compress_func(s.as_bytes()); + let compress = compressed.len() < s.as_bytes().len(); + let content = if compress { + compressed + } else { + s.bytes().collect::>() + }; + Clipboard { + compress, + content: content.into(), + format: format.into(), + ..Default::default() + } + } + + fn image_to_proto(a: arboard::ImageData) -> Clipboard { + match &a { + arboard::ImageData::Rgba(rgba) => { + let compressed = compress_func(&a.bytes()); + let compress = compressed.len() < a.bytes().len(); + let content = if compress { + compressed + } else { + a.bytes().to_vec() + }; + Clipboard { + compress, + content: content.into(), + width: rgba.width as _, + height: rgba.height as _, + format: ClipboardFormat::ImageRgba.into(), + ..Default::default() } - Err(err) => { - log::error!("Failed to create clipboard listener: {}", err); + } + arboard::ImageData::Png(png) => Clipboard { + compress: false, + content: png.to_owned().to_vec().into(), + format: ClipboardFormat::ImagePng.into(), + ..Default::default() + }, + arboard::ImageData::Svg(_) => { + let compressed = compress_func(&a.bytes()); + let compress = compressed.len() < a.bytes().len(); + let content = if compress { + compressed + } else { + a.bytes().to_vec() + }; + Clipboard { + compress, + content: content.into(), + format: ClipboardFormat::ImageSvg.into(), + ..Default::default() } - }); - if let Ok(st) = rx.recv() { - shutdown = Some(st); } } - Ok(ClipboardContext { - inner: board, - counter: (change_count, 0), - shutdown, - is_last_plain: false, - }) } - #[inline] - pub fn change_count(&self) -> u64 { - debug_assert!(self.shutdown.is_some()); - self.counter.0.load(Ordering::SeqCst) + fn clipboard_data_to_proto(data: ClipboardData) -> Option { + let d = match data { + ClipboardData::Text(s) => plain_to_proto(s, ClipboardFormat::Text), + ClipboardData::Rtf(s) => plain_to_proto(s, ClipboardFormat::Rtf), + ClipboardData::Html(s) => plain_to_proto(s, ClipboardFormat::Html), + ClipboardData::Image(a) => image_to_proto(a), + _ => return None, + }; + Some(d) } - pub fn get(&mut self) -> ResultType { - let cn = self.change_count(); - let _lock = ARBOARD_MTX.lock().unwrap(); - // only for image for the time being, - // because I do not want to change behavior of text clipboard for the time being - if cn != self.counter.1 { - self.is_last_plain = false; - self.counter.1 = cn; - if let Ok(image) = self.inner.get_image() { - // Both text and image svg may be set by some applications - // But we only want to send the svg content. - // - // We can't call `get_text()` and store current text in `old` in outer scope, - // because it may be updated later than svg. - // Then the text will still be sent and replace the image svg content. - self.is_last_plain = matches!(image, arboard::ImageData::Svg(_)); - return Ok(ClipboardData::image(image)); - } + pub fn create_multi_clipboards(vec_data: Vec) -> MultiClipboards { + MultiClipboards { + clipboards: vec_data + .into_iter() + .filter_map(clipboard_data_to_proto) + .collect(), + ..Default::default() } - Ok(ClipboardData::Text(self.inner.get_text()?)) } - fn set(&mut self, data: &ClipboardData) -> ResultType<()> { - let _lock = ARBOARD_MTX.lock().unwrap(); - match data { - ClipboardData::Text(s) => self.inner.set_text(s)?, - ClipboardData::Image(a, _) => self.inner.set_image(a.clone())?, - _ => {} + fn from_clipboard(clipboard: Clipboard) -> Option { + let data = if clipboard.compress { + decompress(&clipboard.content) + } else { + clipboard.content.into() + }; + match clipboard.format.enum_value() { + Ok(ClipboardFormat::Text) => String::from_utf8(data).ok().map(ClipboardData::Text), + Ok(ClipboardFormat::Rtf) => String::from_utf8(data).ok().map(ClipboardData::Rtf), + Ok(ClipboardFormat::Html) => String::from_utf8(data).ok().map(ClipboardData::Html), + Ok(ClipboardFormat::ImageRgba) => Some(ClipboardData::Image(arboard::ImageData::rgba( + clipboard.width as _, + clipboard.height as _, + data.into(), + ))), + Ok(ClipboardFormat::ImagePng) => { + Some(ClipboardData::Image(arboard::ImageData::png(data.into()))) + } + Ok(ClipboardFormat::ImageSvg) => Some(ClipboardData::Image(arboard::ImageData::svg( + std::str::from_utf8(&data).unwrap_or_default(), + ))), + _ => None, } - Ok(()) } -} -impl Drop for ClipboardContext { - fn drop(&mut self) { - if let Some(shutdown) = self.shutdown.take() { - let _ = shutdown.signal(); + pub fn from_multi_clipbards(multi_clipboards: Vec) -> Vec { + multi_clipboards + .into_iter() + .filter_map(from_clipboard) + .collect() + } + + pub fn get_msg_if_not_support_multi_clip( + version: &str, + platform: &str, + multi_clipboards: &MultiClipboards, + ) -> Option { + if crate::clipboard::is_support_multi_clipboard(version, platform) { + return None; } + + // Find the first text clipboard and send it. + multi_clipboards + .clipboards + .iter() + .find(|c| c.format.enum_value() == Ok(ClipboardFormat::Text)) + .map(|c| { + let mut msg = Message::new(); + msg.set_clipboard(c.clone()); + msg + }) } } diff --git a/src/flutter.rs b/src/flutter.rs index cd6e51ea1b00..e60063357a00 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -1256,6 +1256,19 @@ pub fn update_text_clipboard_required() { pub fn send_text_clipboard_msg(msg: Message) { for s in sessions::get_sessions() { if s.is_text_clipboard_required() { + // Check if the client supports multi clipboards + if let Some(message::Union::MultiClipboards(multi_clipboards)) = &msg.union { + let version = s.ui_handler.peer_info.read().unwrap().version.clone(); + let platform = s.ui_handler.peer_info.read().unwrap().platform.clone(); + if let Some(msg_out) = crate::clipboard::get_msg_if_not_support_multi_clip( + &version, + &platform, + multi_clipboards, + ) { + s.send(Data::Message(msg_out)); + continue; + } + } s.send(Data::Message(msg.clone())); } } diff --git a/src/server/clipboard_service.rs b/src/server/clipboard_service.rs index eeeea4999c63..c0d081eefffa 100644 --- a/src/server/clipboard_service.rs +++ b/src/server/clipboard_service.rs @@ -1,49 +1,79 @@ use super::*; pub use crate::clipboard::{ - check_clipboard, ClipboardContext, CLIPBOARD_INTERVAL as INTERVAL, CLIPBOARD_NAME as NAME, - CONTENT, + check_clipboard, ClipboardContext, ClipboardSide, CLIPBOARD_INTERVAL as INTERVAL, + CLIPBOARD_NAME as NAME, +}; +use clipboard_master::{CallbackResult, ClipboardHandler}; +use std::{ + io, + sync::mpsc::{channel, RecvTimeoutError, Sender}, + time::Duration, }; -#[derive(Default)] -struct State { +struct Handler { + sp: EmptyExtraFieldService, ctx: Option, -} - -impl super::service::Reset for State { - fn reset(&mut self) { - *CONTENT.lock().unwrap() = Default::default(); - self.ctx = None; - } - - fn init(&mut self) { - let ctx = match ClipboardContext::new(true) { - Ok(ctx) => Some(ctx), - Err(err) => { - log::error!("Failed to start {}: {}", NAME, err); - None - } - }; - self.ctx = ctx; - } + tx_cb_result: Sender, } pub fn new() -> GenericService { let svc = EmptyExtraFieldService::new(NAME.to_owned(), true); - GenericService::repeat::(&svc.clone(), INTERVAL, run); + GenericService::run(&svc.clone(), run); svc.sp } -fn run(sp: EmptyExtraFieldService, state: &mut State) -> ResultType<()> { - if let Some(msg) = check_clipboard(&mut state.ctx, None) { - sp.send(msg); - } - sp.snapshot(|sps| { - let data = CONTENT.lock().unwrap().clone(); - if !data.is_empty() { - let msg_out = data.create_msg(); - sps.send_shared(Arc::new(msg_out)); +fn run(sp: EmptyExtraFieldService) -> ResultType<()> { + let (tx_cb_result, rx_cb_result) = channel(); + let handler = Handler { + sp: sp.clone(), + ctx: Some(ClipboardContext::new()?), + tx_cb_result, + }; + + let (tx_start_res, rx_start_res) = channel(); + let h = crate::clipboard::start_clipbard_master_thread(handler, tx_start_res); + let shutdown = match rx_start_res.recv() { + Ok((Some(s), _)) => s, + Ok((None, err)) => { + bail!(err); + } + Err(e) => { + bail!("Failed to create clipboard listener: {}", e); + } + }; + + while sp.ok() { + match rx_cb_result.recv_timeout(Duration::from_millis(INTERVAL)) { + Ok(CallbackResult::Stop) => { + log::debug!("Clipboard listener stopped"); + break; + } + Ok(CallbackResult::StopWithError(err)) => { + bail!("Clipboard listener stopped with error: {}", err); + } + Err(RecvTimeoutError::Timeout) => {} + _ => {} } - Ok(()) - })?; + } + shutdown.signal(); + h.join().ok(); + Ok(()) } + +impl ClipboardHandler for Handler { + fn on_clipboard_change(&mut self) -> CallbackResult { + self.sp.snapshot(|_sps| Ok(())).ok(); + if let Some(msg) = check_clipboard(&mut self.ctx, ClipboardSide::Host, false) { + self.sp.send(msg); + } + CallbackResult::Next + } + + fn on_clipboard_error(&mut self, error: io::Error) -> CallbackResult { + self.tx_cb_result + .send(CallbackResult::StopWithError(error)) + .ok(); + CallbackResult::Next + } +} diff --git a/src/server/connection.rs b/src/server/connection.rs index 4a383fe356a8..5ea1e923a81b 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -1,6 +1,6 @@ use super::{input_service::*, *}; #[cfg(not(any(target_os = "android", target_os = "ios")))] -use crate::clipboard::update_clipboard; +use crate::clipboard::{update_clipboard, ClipboardSide}; #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] use crate::clipboard_file::*; #[cfg(target_os = "android")] @@ -682,8 +682,19 @@ impl Connection { msg = Arc::new(new_msg); } } + Some(message::Union::MultiClipboards(_multi_clipboards)) => { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + if let Some(msg_out) = crate::clipboard::get_msg_if_not_support_multi_clip(&conn.lr.version, &conn.lr.my_platform, _multi_clipboards) { + if let Err(err) = conn.stream.send(&msg_out).await { + conn.on_close(&err.to_string(), false).await; + break; + } + continue; + } + } _ => {} } + let msg: &Message = &msg; if let Err(err) = conn.stream.send(msg).await { conn.on_close(&err.to_string(), false).await; @@ -2049,7 +2060,7 @@ impl Connection { Some(message::Union::Clipboard(cb)) => { if self.clipboard { #[cfg(not(any(target_os = "android", target_os = "ios")))] - update_clipboard(cb, None); + update_clipboard(vec![cb], ClipboardSide::Host); #[cfg(all(feature = "flutter", target_os = "android"))] { let content = if cb.compress { @@ -2070,6 +2081,13 @@ impl Connection { } } } + Some(message::Union::MultiClipboards(_mcb)) => + { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + if self.clipboard { + update_clipboard(_mcb.clipboards, ClipboardSide::Host); + } + } Some(message::Union::Cliprdr(_clip)) => { #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] From c0e94456020772bf7202ac674faf2f8f3d1a2a7b Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Sun, 28 Jul 2024 17:27:09 +0800 Subject: [PATCH 015/541] refact: embed crate mouce, uinput (#8836) Signed-off-by: fufesou --- Cargo.lock | 9 - Cargo.toml | 1 - src/server/uinput.rs | 388 ++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 369 insertions(+), 29 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 43b8ed497ceb..d994c404d2a9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3777,14 +3777,6 @@ dependencies = [ "windows-sys 0.48.0", ] -[[package]] -name = "mouce" -version = "0.2.1" -source = "git+https://github.com/rustdesk-org/mouce.git#177625a395cd8fa73964714d0039535cb9b47893" -dependencies = [ - "glob", -] - [[package]] name = "muda" version = "0.13.5" @@ -5514,7 +5506,6 @@ dependencies = [ "libpulse-simple-binding", "mac_address", "magnum-opus", - "mouce", "num_cpus", "objc", "objc_id", diff --git a/Cargo.toml b/Cargo.toml index c1a841358693..2879ba91f090 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -152,7 +152,6 @@ psimple = { package = "libpulse-simple-binding", version = "2.27" } pulse = { package = "libpulse-binding", version = "2.27" } rust-pulsectl = { git = "https://github.com/open-trade/pulsectl" } async-process = "1.7" -mouce = { git="https://github.com/rustdesk-org/mouce.git" } evdev = { git="https://github.com/rustdesk-org/evdev" } dbus = "0.9" dbus-crossroads = "0.5" diff --git a/src/server/uinput.rs b/src/server/uinput.rs index f36ad03362b4..942f3753a76d 100644 --- a/src/server/uinput.rs +++ b/src/server/uinput.rs @@ -4,7 +4,11 @@ use evdev::{ uinput::{VirtualDevice, VirtualDeviceBuilder}, AttributeSet, EventType, InputEvent, }; -use hbb_common::{allow_err, bail, log, tokio::{self, runtime::Runtime}, ResultType}; +use hbb_common::{ + allow_err, bail, log, + tokio::{self, runtime::Runtime}, + ResultType, +}; static IPC_CONN_TIMEOUT: u64 = 1000; static IPC_REQUEST_TIMEOUT: u64 = 1000; @@ -34,7 +38,10 @@ pub mod client { fn send_get_key_state(&mut self, data: Data) -> ResultType { self.rt.block_on(self.conn.send(&data))?; - match self.rt.block_on(self.conn.next_timeout(IPC_REQUEST_TIMEOUT)) { + match self + .rt + .block_on(self.conn.next_timeout(IPC_REQUEST_TIMEOUT)) + { Ok(Some(Data::KeyboardResponse(ipc::DataKeyboardResponse::GetKeyState(state)))) => { Ok(state) } @@ -171,7 +178,6 @@ pub mod client { pub mod service { use super::*; use hbb_common::lazy_static; - use mouce::MouseActions; use std::{collections::HashMap, sync::Mutex}; lazy_static::lazy_static! { @@ -389,7 +395,7 @@ pub mod service { } else { match key { enigo::Key::Layout(c) => { - if let Some((k,is_shift)) = KEY_MAP_LAYOUT.get(&c) { + if let Some((k, is_shift)) = KEY_MAP_LAYOUT.get(&c) { log::trace!("mapkey {:?}, get {:?}", &key, k); return Ok((k.clone(), is_shift.clone())); } @@ -431,7 +437,8 @@ pub mod service { DataKeyboard::KeyDown(key) => { if let Ok((k, is_shift)) = map_key(key) { if is_shift { - let down_event = InputEvent::new(EventType::KEY, evdev::Key::KEY_LEFTSHIFT.code(), 1); + let down_event = + InputEvent::new(EventType::KEY, evdev::Key::KEY_LEFTSHIFT.code(), 1); allow_err!(keyboard.emit(&[down_event])); } let down_event = InputEvent::new(EventType::KEY, k.code(), 1); @@ -504,7 +511,7 @@ pub mod service { } } - fn handle_mouse(mouse: &mut mouce::nix::UInputMouseManager, data: &DataMouse) { + fn handle_mouse(mouse: &mut mouce::UInputMouseManager, data: &DataMouse) { log::trace!("handle_mouse {:?}", &data); match data { DataMouse::MoveTo(x, y) => { @@ -515,9 +522,9 @@ pub mod service { } DataMouse::Down(button) => { let btn = match button { - enigo::MouseButton::Left => mouce::common::MouseButton::Left, - enigo::MouseButton::Middle => mouce::common::MouseButton::Middle, - enigo::MouseButton::Right => mouce::common::MouseButton::Right, + enigo::MouseButton::Left => mouce::MouseButton::Left, + enigo::MouseButton::Middle => mouce::MouseButton::Middle, + enigo::MouseButton::Right => mouce::MouseButton::Right, _ => { return; } @@ -526,9 +533,9 @@ pub mod service { } DataMouse::Up(button) => { let btn = match button { - enigo::MouseButton::Left => mouce::common::MouseButton::Left, - enigo::MouseButton::Middle => mouce::common::MouseButton::Middle, - enigo::MouseButton::Right => mouce::common::MouseButton::Right, + enigo::MouseButton::Left => mouce::MouseButton::Left, + enigo::MouseButton::Middle => mouce::MouseButton::Middle, + enigo::MouseButton::Right => mouce::MouseButton::Right, _ => { return; } @@ -537,9 +544,9 @@ pub mod service { } DataMouse::Click(button) => { let btn = match button { - enigo::MouseButton::Left => mouce::common::MouseButton::Left, - enigo::MouseButton::Middle => mouce::common::MouseButton::Middle, - enigo::MouseButton::Right => mouce::common::MouseButton::Right, + enigo::MouseButton::Left => mouce::MouseButton::Left, + enigo::MouseButton::Middle => mouce::MouseButton::Middle, + enigo::MouseButton::Right => mouce::MouseButton::Right, _ => { return; } @@ -553,9 +560,9 @@ pub mod service { let mut length = *length; let scroll = if length < 0 { - mouce::common::ScrollDirection::Up + mouce::ScrollDirection::Up } else { - mouce::common::ScrollDirection::Down + mouce::ScrollDirection::Down }; if length < 0 { @@ -621,7 +628,7 @@ pub mod service { rng_y.0, rng_y.1 ); - let mut mouse = match mouce::Mouse::new_uinput(rng_x, rng_y) { + let mut mouse = match mouce::UInputMouseManager::new(rng_x, rng_y) { Ok(mouse) => mouse, Err(e) => { log::error!("Failed to create mouse, {}", e); @@ -650,7 +657,7 @@ pub mod service { rng_y.0, rng_y.1 ); - mouse = match mouce::Mouse::new_uinput(rng_x, rng_y) { + mouse = match mouce::UInputMouseManager::new(rng_x, rng_y) { Ok(mouse) => mouse, Err(e) => { log::error!("Failed to create mouse, {}", e); @@ -761,3 +768,346 @@ pub mod service { log::info!("stop uinput control service"); } } + +// https://github.com/emrebicer/mouce +mod mouce { + use std::{ + fs::File, + io::{Error, ErrorKind, Result}, + mem::size_of, + os::{ + raw::{c_char, c_int, c_long, c_uint, c_ulong, c_ushort}, + unix::{fs::OpenOptionsExt, io::AsRawFd}, + }, + thread, + time::Duration, + }; + + pub const O_NONBLOCK: c_int = 2048; + + /// ioctl and uinput definitions + const UI_ABS_SETUP: c_ulong = 1075598596; + const UI_SET_EVBIT: c_ulong = 1074025828; + const UI_SET_KEYBIT: c_ulong = 1074025829; + const UI_SET_RELBIT: c_ulong = 1074025830; + const UI_SET_ABSBIT: c_ulong = 1074025831; + const UI_DEV_SETUP: c_ulong = 1079792899; + const UI_DEV_CREATE: c_ulong = 21761; + const UI_DEV_DESTROY: c_uint = 21762; + + pub const EV_KEY: c_int = 0x01; + pub const EV_REL: c_int = 0x02; + pub const EV_ABS: c_int = 0x03; + pub const REL_X: c_uint = 0x00; + pub const REL_Y: c_uint = 0x01; + pub const ABS_X: c_uint = 0x00; + pub const ABS_Y: c_uint = 0x01; + pub const REL_WHEEL: c_uint = 0x08; + pub const REL_HWHEEL: c_uint = 0x06; + pub const BTN_LEFT: c_int = 0x110; + pub const BTN_RIGHT: c_int = 0x111; + pub const BTN_MIDDLE: c_int = 0x112; + pub const BTN_SIDE: c_int = 0x113; + pub const BTN_EXTRA: c_int = 0x114; + pub const BTN_FORWARD: c_int = 0x115; + pub const BTN_BACK: c_int = 0x116; + pub const BTN_TASK: c_int = 0x117; + const SYN_REPORT: c_int = 0x00; + const EV_SYN: c_int = 0x00; + const BUS_USB: c_ushort = 0x03; + + /// uinput types + #[repr(C)] + struct UInputSetup { + id: InputId, + name: [c_char; UINPUT_MAX_NAME_SIZE], + ff_effects_max: c_ulong, + } + + #[repr(C)] + struct InputId { + bustype: c_ushort, + vendor: c_ushort, + product: c_ushort, + version: c_ushort, + } + + #[repr(C)] + pub struct InputEvent { + pub time: TimeVal, + pub r#type: c_ushort, + pub code: c_ushort, + pub value: c_int, + } + + #[repr(C)] + pub struct TimeVal { + pub tv_sec: c_ulong, + pub tv_usec: c_ulong, + } + + #[repr(C)] + pub struct UinputAbsSetup { + pub code: c_ushort, + pub absinfo: InputAbsinfo, + } + + #[repr(C)] + pub struct InputAbsinfo { + pub value: c_int, + pub minimum: c_int, + pub maximum: c_int, + pub fuzz: c_int, + pub flat: c_int, + pub resolution: c_int, + } + + extern "C" { + fn ioctl(fd: c_int, request: c_ulong, ...) -> c_int; + fn write(fd: c_int, buf: *mut InputEvent, count: usize) -> c_long; + } + + #[derive(Debug, Copy, Clone)] + pub enum MouseButton { + Left, + Middle, + Side, + Extra, + Right, + Back, + Forward, + Task, + } + + #[derive(Debug, Copy, Clone)] + pub enum ScrollDirection { + Up, + Down, + Right, + Left, + } + + const UINPUT_MAX_NAME_SIZE: usize = 80; + + pub struct UInputMouseManager { + uinput_file: File, + } + + impl UInputMouseManager { + pub fn new(rng_x: (i32, i32), rng_y: (i32, i32)) -> Result { + let manager = UInputMouseManager { + uinput_file: File::options() + .write(true) + .custom_flags(O_NONBLOCK) + .open("/dev/uinput")?, + }; + let fd = manager.uinput_file.as_raw_fd(); + unsafe { + // For press events (also needed for mouse movement) + ioctl(fd, UI_SET_EVBIT, EV_KEY); + ioctl(fd, UI_SET_KEYBIT, BTN_LEFT); + ioctl(fd, UI_SET_KEYBIT, BTN_RIGHT); + ioctl(fd, UI_SET_KEYBIT, BTN_MIDDLE); + + // For mouse movement + ioctl(fd, UI_SET_EVBIT, EV_ABS); + ioctl(fd, UI_SET_ABSBIT, ABS_X); + ioctl( + fd, + UI_ABS_SETUP, + &UinputAbsSetup { + code: ABS_X as _, + absinfo: InputAbsinfo { + value: 0, + minimum: rng_x.0, + maximum: rng_x.1, + fuzz: 0, + flat: 0, + resolution: 0, + }, + }, + ); + ioctl(fd, UI_SET_ABSBIT, ABS_Y); + ioctl( + fd, + UI_ABS_SETUP, + &UinputAbsSetup { + code: ABS_Y as _, + absinfo: InputAbsinfo { + value: 0, + minimum: rng_y.0, + maximum: rng_y.1, + fuzz: 0, + flat: 0, + resolution: 0, + }, + }, + ); + + ioctl(fd, UI_SET_EVBIT, EV_REL); + ioctl(fd, UI_SET_RELBIT, REL_X); + ioctl(fd, UI_SET_RELBIT, REL_Y); + ioctl(fd, UI_SET_RELBIT, REL_WHEEL); + ioctl(fd, UI_SET_RELBIT, REL_HWHEEL); + } + + let mut usetup = UInputSetup { + id: InputId { + bustype: BUS_USB, + // Random vendor and product + vendor: 0x2222, + product: 0x3333, + version: 0, + }, + name: [0; UINPUT_MAX_NAME_SIZE], + ff_effects_max: 0, + }; + + let mut device_bytes: Vec = "mouce-library-fake-mouse" + .chars() + .map(|ch| ch as c_char) + .collect(); + + // Fill the rest of the name buffer with empty chars + for _ in 0..UINPUT_MAX_NAME_SIZE - device_bytes.len() { + device_bytes.push('\0' as c_char); + } + + usetup.name.copy_from_slice(&device_bytes); + + unsafe { + ioctl(fd, UI_DEV_SETUP, &usetup); + ioctl(fd, UI_DEV_CREATE); + } + + // On UI_DEV_CREATE the kernel will create the device node for this + // device. We are inserting a pause here so that userspace has time + // to detect, initialize the new device, and can start listening to + // the event, otherwise it will not notice the event we are about to send. + thread::sleep(Duration::from_millis(300)); + + Ok(manager) + } + + /// Write the given event to the uinput file + fn emit(&self, r#type: c_int, code: c_int, value: c_int) -> Result<()> { + let mut event = InputEvent { + time: TimeVal { + tv_sec: 0, + tv_usec: 0, + }, + r#type: r#type as c_ushort, + code: code as c_ushort, + value, + }; + let fd = self.uinput_file.as_raw_fd(); + + unsafe { + let count = size_of::(); + let written_bytes = write(fd, &mut event, count); + if written_bytes == -1 || written_bytes != count as c_long { + return Err(Error::new( + ErrorKind::Other, + format!("failed while trying to write to a file"), + )); + } + } + + Ok(()) + } + + /// Syncronize the device + fn syncronize(&self) -> Result<()> { + self.emit(EV_SYN, SYN_REPORT, 0)?; + // Give uinput some time to update the mouse location, + // otherwise it fails to move the mouse on release mode + // A delay of 1 milliseconds seems to be enough for it + thread::sleep(Duration::from_millis(1)); + Ok(()) + } + + /// Move the mouse relative to the current position + fn move_relative_(&self, x: i32, y: i32) -> Result<()> { + // uinput does not move the mouse in pixels but uses `units`. I couldn't + // find information regarding to this uinput `unit`, but according to + // my findings 1 unit corresponds to exactly 2 pixels. + // + // To achieve the expected behavior; divide the parameters by 2 + // + // This seems like there is a bug in this crate, but the + // behavior is the same on other projects that make use of + // uinput. e.g. `ydotool`. When you try to move your mouse, + // it will move 2x further pixels + self.emit(EV_REL, REL_X as c_int, (x as f32 / 2.).ceil() as c_int)?; + self.emit(EV_REL, REL_Y as c_int, (y as f32 / 2.).ceil() as c_int)?; + self.syncronize() + } + + fn map_btn(button: &MouseButton) -> c_int { + match button { + MouseButton::Left => BTN_LEFT, + MouseButton::Right => BTN_RIGHT, + MouseButton::Middle => BTN_MIDDLE, + MouseButton::Side => BTN_SIDE, + MouseButton::Extra => BTN_EXTRA, + MouseButton::Forward => BTN_FORWARD, + MouseButton::Back => BTN_BACK, + MouseButton::Task => BTN_TASK, + } + } + + pub fn move_to(&self, x: usize, y: usize) -> Result<()> { + // // For some reason, absolute mouse move events are not working on uinput + // // (as I understand those events are intended for touch events) + // // + // // As a work around solution; first set the mouse to top left, then + // // call relative move function to simulate an absolute move event + //self.move_relative(i32::MIN, i32::MIN)?; + //self.move_relative(x as i32, y as i32) + + self.emit(EV_ABS, ABS_X as c_int, x as c_int)?; + self.emit(EV_ABS, ABS_Y as c_int, y as c_int)?; + self.syncronize() + } + + pub fn move_relative(&self, x_offset: i32, y_offset: i32) -> Result<()> { + self.move_relative_(x_offset, y_offset) + } + + pub fn press_button(&self, button: &MouseButton) -> Result<()> { + self.emit(EV_KEY, Self::map_btn(button), 1)?; + self.syncronize() + } + + pub fn release_button(&self, button: &MouseButton) -> Result<()> { + self.emit(EV_KEY, Self::map_btn(button), 0)?; + self.syncronize() + } + + pub fn click_button(&self, button: &MouseButton) -> Result<()> { + self.press_button(button)?; + self.release_button(button) + } + + pub fn scroll_wheel(&self, direction: &ScrollDirection) -> Result<()> { + let (code, scroll_value) = match direction { + ScrollDirection::Up => (REL_WHEEL, 1), + ScrollDirection::Down => (REL_WHEEL, -1), + ScrollDirection::Left => (REL_HWHEEL, -1), + ScrollDirection::Right => (REL_HWHEEL, 1), + }; + self.emit(EV_REL, code as c_int, scroll_value)?; + self.syncronize() + } + } + + impl Drop for UInputMouseManager { + fn drop(&mut self) { + let fd = self.uinput_file.as_raw_fd(); + unsafe { + // Destroy the device, the file is closed automatically by the File module + ioctl(fd, UI_DEV_DESTROY as c_ulong); + } + } + } +} From 50dd2b3aad492e799b7fcdcec56b3683dcf2279a Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Sun, 28 Jul 2024 19:33:00 +0800 Subject: [PATCH 016/541] chore (#8868) Signed-off-by: fufesou --- libs/clipboard/docs/assets/win_A_B.png | Bin 43619 -> 470678 bytes libs/clipboard/docs/assets/win_B_A.png | Bin 43447 -> 492821 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/libs/clipboard/docs/assets/win_A_B.png b/libs/clipboard/docs/assets/win_A_B.png index 87fe702dd9b6b39c024eafcc21830aabc4496a39..9281c20dc6307384797e7dd6787a929d6eb33e03 100644 GIT binary patch literal 470678 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 z�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 z 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

k<;{#t32sNSJ{umnBxtY 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

QuDErjM@HossIzwbiMG0 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

C2U4l>? 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

CiPjeLt32{VWJ7P7vOCSs@Jyc=@GoGpyba{GX# zcoaIHqs2sz-zd@@EtA1B)HUW0P;wC*v0!tgd1-0o1NkG5211P|m9W^v$z@~lr}oFF z6ATqmyfa^iBAh9$?3l=l!hwbe8b1N<%D zbA^$8IItSNd%=YYO7uD_9g$iN)wGiJBT+>2i*e0`y)RG6x6Pc&mzbuYp^dkdB@W%4 z!ItCZI?;WpL5?r-&Q7ZoOKK@2mNjIPN*Ok^UB;kVQ(S%SD%aLgpQXUVVI)*BJIJf%6d4Lw&(piz;3c1A7TSrm98I`Jx!0NlyV*7;j_*X{7gAaq7Vw z!ZO@vquHAdD$RDOy*aQ25gM_Id?9Kd)*T)^`Rd=DK}}U)?9}K^CjOy?$%|ZML7PxZ z!RoA|d@}9#M|_eWI%I=wH=8v-pib638-k;yr_!HZ3^yp?x61qO4ko9^r*<<&R&=*;Hm>xNK(MEg=wZ?l7ef%c`lgE=g7L z;0amY2??k#Z{tfCaVFH&OJYf5DQzzvB`Yy2;bB+iV*t>I*WX+;l*Pt2 zOUR)hYz&S8Tj30g8N$&dF~8LGTeoZKYs{Vim;4$JRB)BnLY@fF zIh%XEaUIcxjWc4PC6s%$@|ewtN-R%AsGqq^70y3mfcerea@6!^R7{= zGRTxwCHi|cz_yha4U*h$)j!p_RMO2So4ttoJ?BcDcOLJ2c-XvPs8bVa9^-OgX|lQ4 zM}gn?1wzoF(ijLWK#T~Cyq6d@v_xt$ln(Rhb z*|X~C9I-|=&}Qk!%b#>%n61-T2%ig9j-Xe?-oqy(K0uZd&IH)vn?fhXrwDq);Z?%1*S8iutX?LRiepR zHcA6BbuFv@~*@C z>dK!0%ltW5l;m^)oilMGybjFU$VWuK!Kbi?Vj0klSm=BiYmg-4!@7AH8#%aq$Eft$ zG8bsZ&w8JfM@8^`ddTT&GzH#u(RDBZV+WVV2Jr*1XA%;1(7r5pF2$oxdKzqUS|H7~ zs(d{qhQP+l=>l?<9cg$`_Po^t!qMPWxjhN5aPNNgq+l?qq*McF7@SnnYjE)&MjUC( z*jZt0qQ@1D`GR`6+u`F!F3arsPiQi&12Kwq<~8PVD^B|DyLD#(%XlcGVn?Xe(#S<= zRQR7<>8E9SWxrS2957*6uCdQc6<-2a$KKRFdC2ZIr5x|U`**Eoz&4e;h(w9Cb?@$1 z{+7RHcEkRpsU&dYmor4?vRJ&bRwEbvG_eTYy;Uv@pk`x8nE`sl+LnOYwGO?-&X=PP zeJZoNpR4RaGEj8OIT?^aymz?DrZL0TwppltV8fkvy1?msB|E(+971;9+AKT;7~D8l z{Z~>`JO{cqa%!d)0~e}ltZo$LHDrT^nM9hZ`$;G!OPH6eLR9GJG-VH&Z=jpNlH8Ap`yavV@4PDy_z?v12FK)FSd!p zcL|tPGgh?0OPy0>9ydchW?#>GB}0?KO`-_K%zauf<=>qNJL#Rb!jM~3dp?Y}4A|tH zD#abMD3qAcR^2v^mvS7?FA+lPYl`8kPlQ#=2r;9~RY#e)6*{ zRPowoF+dZfwtLQ=u>dSVX%7iyBL$ZcJ8sN5#tSB-`<^-x+UQ>#nMy!eeYus00O%K7 zM)g9OC)9jH3&Dgkaf?%i8D#13C$k;5PZ)dCd8!cS_WXngt03)0^*dQ-)`S+$y}Sx4 z)aJBbU5koLs`OWaal$x~DI;}4jIU~)^#`#`ZofmQZSw|hSy|F*#C^Hnv!pcz{mM=z zc^5tL^T(jAf?hR>+sZZ2S9qQ5!R9%K;A};|31p2bB`KS}Ahv=(=J9xjwaCdhjKwyy zHVPwM-2TVH>ud0Dq>ga%X8(_qSMZ~s_b8blpcMt->Frh@;n^(yz~F8x$w(GT2{@B* zew7&^*E`Mh@Qot#$K^(7kw!ou1GL=>6%{q)=#6kV)pvKn;b`d{UCGJkAsH&dXepGl zQ*=}pbRjZ=(NC$D>+1{N5)zFL+K7>Hb?anM6I8OlJ{)2ggJTfyRj~>Z_9pizP^V(p zjsJ!&DbAa6e9Ihb5lT-poEvT8{F|~Z-QcTYP4WHuvMT8L(4}dai+=9I0sr(#Na^rC ztHRfN(N?B9zsWyDhOh%x4VT%x(saJ-8&~b=CGOAMYdfy5AB74(wZP{Stwliz6&w`w zbH{2$fl0*WO8ncu3_nIknTDUpx%o0zQ}9hyi@qC8=6*w0{jPWldrf$*6aCoJb;lLe zaF3}9Id|2!0x6$n-`b%0L11$^aAU(~`h(01?7zSI3+3r8fqX${QlulZ)~;{sJ|_#! z%h@js)WznO5Ww4`4Hpxj91QiAFAeDW^8d(M+XI*EY> zCEqZ&6unK&FJ*D0y#_4V`+$5FKz1N;-Gt(aFM;ffckv=F&h@+vM8n_P)<%O8*= zvvQdqJ>nq(T>!l>Lk(y_KM&~`G0YB%g7!TjV#CF039If-Iy)uN3NAbr+B2EV>atBJ zS!Yl(_|xM#9A1&DyeUD*qnvATV%%$dxa3VpzZ_LNRLxNsm3Xx%e)rsn04jiF}>Hhh2%LHT3TVO#Lq+YBzp6>{mTjUWN$Lqn@V?ctZ>KE zinDQG`Cff9uOG6$c8AgH7nu?%o!4k;i_4Pt_M^->2}4U%#PF0| zA>V|h@P;|g2o2g*W0AN+uA$$w@_#W`+W&cEX_LyR9;Dft04@knAAmFXvw8*RHePAe z9dEE`(ED9De=$z2X!`FdZ$ny+zo(w}+R}F~xq{J_$u({K2PomzOTrNCwoqLW}jwD4f-MuA}?Qef=kI*z&X z@o6g7Hn$4H!{W-yP4Jb_dH0RoZfIKz z!gn3}N>6KyqyBq*^@CPcc2IqJ=9*{X-cBMr>G!51a2np0&)DaQtUW(r;Z>k7G>qJ3 zFQdp_ShLD%=wfgV#nlDTt*3AyJr{o%^<%@l9w6yy+G(x&{ekSflVAl<7Me%|eB&v? z&2`tGYsc1DdQZapfLD>v+Pkw(GDGGq`F1bSc_4nB{!IF1pMYd%?IxS=9u)GE2Yu(~ z0sV*p5F#AkL_F2tMaX*B>@q(OxF%6itU2K0l+(RJILe1eLU+yeK13oMD79U>;O}!{ zGqa8(6@bvr4?9oNoW6>m0f(3}%^1=&QStiq#5=7<{XuobP3giP6j9Cm<;lZY+4A^( z-s)XH>n@#Va~fkGDi`LiT;uBH`&hM09^sWcq1Ft=NLpjW@xE3BN9RH&(AA<}4Q)ka zF3!aKeRrr%_S))9qo*7RAwv4Q0slDC{1X4m=t1XeMIv7hm(0!3=%J&#dGk zvcnu21*7829Xv2^`_%!vdt{n4RU&u7a$Pm8j7#Yw?3t{aclbPGy?krVrv_~G+dn;Y zksY*aqV{ZZ{E>AVW&}`oq&vG4#++0Sv!V(F!I)=x_1de&SG-yLNJ$SDu=l_SrO?iZ zw3_lF%0=Mga={(C8Y;RBckDZM^Ejw0@J@!COD6NSG-1+OCR~JcsDG8?m-Tg+{Q%7b zkn;;7*mY<+zY;&^c2Gc#XKH8rzDNPf2Yu%WF5Oh+@^%~s3JO2?L=%DMSO7Y7V^f1- zTIlCvY&Uk56SA2PpL}0tQx=8nimit!A0af_T~9I^kmYvuQrJmTka0(Y1>}%q1~#Eq zN*_-vmto=EfS0Y>Kba7LQnbYOXs9^Cb_Qw}WvPg{lO~+LKDdE#Wj^`JacA=y`W`)$ zC;A4wUDJpkaVQ?`l;}^`X7Z@{li-z)5!FuVDbEi`yc$rUG~4;Qq+pP9HE)z@L+j)6 z_k#fxA`9n!NI8F;8ZnPc%E~zUDqOinVCp2Es3e}JlnerRF_3@Xs%7W!+Xy2sD1Qry?^*k3fqJv8c!jp{zzBAj}FeQC+00k)5#l;0IM4)t*QF*AqAtE%6`N z?rc7NjY1bAzV+6HiYFD_`l-*`Sy-w+qkGa_>Gx=FL_UDd7MRZ4yn!4W;t=&?!+cT! zPcieqXnP+6R`+t)OsA>xxKLdDUn7e+{`Yj5L(UP5DaHsJ9C zVKs$71h8+DDpleIiOXfh{Ugqffy0={EMBQ@6R1R=B%-%+2PCJOH8d!Szlv26FSHvp zcDKfGU=+zEH5CHKX+qWCampC}NF>pnv=v$YNkfk&Y$Fq^r)Nwenc>iqnKF$<5LV#K z5SK^??uWh!F^=P~!UmR70zTblW&Z?WvkYD5aM_WRAjJj5Wf8yfH-8UG74Is)I+Tpw zrB<8MZzAx1KP&Jn#&wm>w{S`$xoB^LA~yfK+ZW6ug&Kw+VDD$JfSzEiH&Gd$G)6~= zYGZVwB1&%fK5I+Nda*pf=aup+RLS|_47H9K;v|^&R7kIU4+=!`I+cZfBAwwby8pwo zcvn#Nwzz!8yLfX2aYIKEJtS7eke{o&fiR5^*p9)t~|=u9X|D>H4IoZ<0nbkp@KS-8^B*Q}54-Ux@u zJv$e-?Drqf$-IN-7Kv7P>_*>J3l>i=n&TEq=Wd+p!yVv8<$y+*7{vzgC_ihNl?>oV zDBdTalJM{@^)Wi{n0|}=<1G}cG>#wl{#}ap*3Pu2q@&V>NW!*gIGK!al540hd<~ve z4n}%O&QhQ6FbfAq_&j1$@{(q*#hx9Jd-rVN^Ox)#0EJCLpAAsAN{xhEP* zX7}5&*sMO{jQ@=7fg~x;QVypKPsml4}o4RgzrarE23 z*77@DNSxkH%SGJI%L|2dXfd_CCbR)6sWxbx$5!|F!h%$hwl3UgJ?nmpJN@yp8c%<# zXQt_t_R|wg9+jewRup&EeT)&yE9Tn>oksx#K#hiZane!KmE+rBc7&a(4{0=hfUL5M z6lE2<9VdB3T#VI*zUtfl>_i#1Ng~MsDBf_1gZAAGt60XNq9Kdu-aW6-#HlN3Jz18$=$82mays zYeZAm^YnD_+{w0d<@PV5c?HYAX&TE^6tGoJF_Vlmw-24UX@AeTEcY2TEz8TMv3Nc1 zzU_U~71x^LM{)Yc2#dHBZ~lK~jmiXYfry)3WAU%5%;+PIHDC<|ykH5^>Au6sn=JyW za1}#aDdO|Ckv-E!6FirU9CJ8VS7+6X4NaO3eytTJo~~i6qQOc?oMMPj%|V+q_abHm z2-&1^(g8>6@ZCf;P~7tR1S6s1RR^Z6z=GY7*m9f;lHu-rz4M29()1hy;=~t+zX9{{ z73eK*X*)ZG7M)p&7SgH;Gyr}<+k+x2o}J~y>jW4#MCUqt+TEsZ zJK8D4bpnU)@Tv?I)7*yT9U`2{8|e@EYiC}yEaq1yM9@f)=W5wwF3sXN0v9zS#k`epL?+|e&6uKt z?zQ8&yh!O(5V?h!!l+S1)M{9tgLg!DihPcK5G+!J2~^;A`?Aq%UHY#zl;id!)JQk? z1*js9gcr>~Zs0Z_Y?W=He2C0>OWq}!`O=A!0-`K?ao%&M8>)}2aUuahSVvLG*q7Hb ze6_nVWy$navmWB}$%9_6VR7Dp!5nhV(J}loi*@l$HTvgF{y-^@r3)+c2yq+0geDOS zn7k`t{i0}xPCJQ7Rf@(3e7Gtr{&E)`jQMBP%2p>Lvz~|zbLrq^+ zV935&CqB#z6!YK&Bxe9UyU?Kg<8l^1<;%8;q*PpRh=lLKuemy;r+_Wfxy3X)(IO&C zAl;>4E#qQrD>;%NskI)@*Q(QevTLrRmxKedN2<8w_X>BOam5acMDaHTIm|vpB@1+1 z(voZd6MTs*K@E~yK$8qy?ULy%2SKxg*UpOGD19&##(k9J0qBrK7tYHGJE_!%~m$p%RBj0@FpT$a?1{Z!hVh2UCPASD{GVYZr0C4(Dhwqp;n#G`*mdpr3yY@4q9 z;_&OF`?lRb8G=rC)J?zR&c@0~Rln^*+vx?QY)bOOfbKbG&5E! zi`j)Kf!ms&!tR!g)nZ&i^>g!no#umt1E2typ86HGXgmPxQZJD%J$Y$x4vH*!tdvP1 zDFjHPSv~#9UsEpY`|zy6d2^zjTXf;HbHDMLGR@hZ*~$0>Q*Rc;t?xWE4e}+Wbh`7R zu4O+BVvg|Lc|%*i67)>=%;7tCLiR9l+&OSU*Ne!-6R>#len$-y-n&t-VNO;$(DT2r z7t-cpoqvc$XsHbkX5A)#{dfOX-}m3*%M%5Mmt%gH-_WI`xn1ysML6PQelQ3+CQ%*TM7_<#vaa6?M8ZI2k76Ea#K7k zAxCtT7{K?kQC~RU9BwBD1~W`!eTkqqMb8;=-dv@{k4){iZ7eW(#n0Tb)jmautNgv~ zmgY)TsExvl@-xYy;-doJOb8l(Sm;3cLlMrFm**$OE1EX*(Mr{e3&H(2wz|v%`;6Uw zZH_^Q=7$4-Q9?}}DJrXqMwAKUNBUnE{$8 zo}0s;a*T)(H$?oD`*Zz9J24|qcqEk@b9P3I)B=F4;l`odPLR$7y zLdOuGkW_HK=+^*Cy*u8Rj^e;4=RWyxWqWKRD(b?}y@*KYDS#cEz`v=cR=dVcD3#Ufwv8UAjs9UKs+2k4Y zpJVeCU#f>C*Q7qiT|NMzY=UORe8uhMdNRJUq?O2-QiKzPZ>#=H6#c0ON0(;WQUl1w zI%4n6+J>K-_MiM4LRZHpX28(7LbdMV&fCu*czo>NA9LZXTT{^o3it9}q4p)@jUT67 zzxWAG=dkIp-p8@J6kMT|x7HyBe?ftZO@g4(_Aw!VQe#e>t2`kkD@%`*JSa*s?wL{p zHFMHaJ71`@=u*BDStpaBQTUj0HuB1}v)I0)_}kuPszr^jW=COtrPY+=>eBt~fr?=8 zC$_(Ik9MTGJagZ7Vv~#I{Pczsp5(}awe+s4Ce#)jazRHFPjbQtouSf_|DI?yS$&7J z^Fvy7H#>Oghs(!K)XHdGvVq$D>2${96#k$4+kP4&ds_YlLd65(OUh+916D_p_9N0q zr7XP1`eph^(7+Oftl+Pq6Oir(jybn45y`%Y9X_>nX<&hAWB$yxhfq&hedTysCqpmADhYl}cGxrOHyuuo1MYZC?nl50 z0iR`Y0nBoqA31PhpRpSC&Evw(=INn{5Mq@)a6*$wa-!wuryOz9UH~uDnEo4GW(6sH zY9ie1>NDZ!k)G*NAC`cW1DpEXZ0inaW-?3zVuSjgD8f2N5A_T^iq z=>{MAs3tjXcCrhbPkOZm$K=G(2RoMC>|>`Y&mOwforkc;dd1`|dun9aS2S?lqf@Mjv% zB2`R-C0>PwXn~EzCc5M{J(S3DaDJSgACOiZdvs{yls|aq%8T5ks+=orR#nRK(qM2u z4&T-I`l6{bvEI3iUfhc<4+4ru^<@P_hgE34h}0DC2&$xp$^Qgfy;Xo#@Ak!>O>1U? zSP^!x%TPXX$CW~Y+OM5ZwH1yufm(u^ zAuJ{2y{X;ksS4IYw#|?j#dKGr=;lI%T-2Tf@=sSS6gw%P&OqZ-iT_1%L>^-b-D$w3 z;=eDAC+06PVcXdK!2)9CrjL!i3VV{CH$s&?p~a3IAtrZI9r%Hs&T|(IVw|BLwZ|5X z0^cCZ2gi6)1mNH$0m7!S6U)GKCK3cmgjN?R-e^~+9Q6mwedyGSq5z4_X`+zr$bK_| zJg}O#0zuH=>npAlP_8P1XzlZ zECdIevQf0H${s%I)X1K={PqaUSeefk!Z0{*@MB&rN>o$qKnu2|!j=MXGFOt*-MwFw z_%@`+AM2Qk)rk7cW%Aw^eiD5H?lifX?xl~2T>2i%ww-pNkD{erZy&~SM>IlfWK90} zocVtOyAal%r#yTE3I3X)pNO*|71$VqsSiZtc=baIXnjR_#Hflt1xTRL#k#Mi41(=< zBl3hYY4XdS*VMZ~j8zYghR=B9B+1`*yZw^6HgvfOp@G?17+V;9-jj`xcL5TeQ6EDk zY(??7i^Maje$^N3aHZ*`En_hN?KmS2igf6O*t%Ud>+!#Q8?E{%!hb699wY> zUU?mK7Z};e*75oVjb1iK6Y+9?Z@1uv})Qp)-aC(=W--UrijaMPm!#b%7m`< zSaKlkGr>HP+m7>brr4BKDPK-XnHLIMf8VNjrp|9s{MPK;`38JWP_WxABv-oQWW~X z{w6njcSXX(y(sZ@$jFU0qrdM@PnYFd*Yg1gw{eA!;PLqqw_n;G#}GUf9~sN<@I^0k z**yE;-^8@}e>jsb3j;6PKb9IBS^qFf7N`hCe1QmUTc7kg#XC0%myqUOO$I2zooS=# zfiV6x{qWq=ARle>*){zZ#ytlk?%HYQv9=MNP50jSL9>55ww#(3^xR z8ufb)Qq9o5(;&T6>XG40vAZaW^K6(;Soh&Av!)^)zW&Un>29)Rqtj~CfGIX_B&G;w zem)oNaV^6KRSJDD;%~Q)ZFb44Q;b`w3U%LCZCUQ$|@wC-d8#t!IOVx8poi8@*Fn$LN4v-}Wk zw0s|>0i?7n2}9FB`@%wf9C#|7>XOLv_F-EkzO#JiIx>YDrOIQ#e*4m;P54aV6dUL) z+kYPZY;l6*^(BYhgJj8_3|}^j_58ti{f1(6!Ri7{uqJfRKIg`U(BYtK@p=;alwU++ z#`saLTWGa%_+%iMc&%m#poEyMG|q(o5c~m=$8nq{&+1}9`twuy9^+?BiVeHqdDVC< zwT4?L`LuAQJ*c+$f^fzM=f60$|G2?U6cFJQh_MRq5=7CZ~Q+u7a@9L%2pJ(D~?Ogu+acHGIG2*jzzAIWlS zYG|x0``yKToA(3NggUXxb3zJe#Qa8BsywzcCDcP|1G;~HsgVd<(N~j@#kyN;d&8!L zYUTYm1)1YL8&}px)1U(}i-m|c%ADkJ{a6YOMs@tqckMOF#@c~#_{{6SYe^$;-~j+F z=Ev>}0A^k%&8SYaLNl@9N7@RMIf3D|EyZG&!8L{HmgE#KP!n$`{9Gxq{ zb4;6bPl_11xLY#$1&LCejh=YAP(wvkmbw1Qf%#AKlpZj0{AeTOL*UmUTfN2Vuk9?y zF4||#W6cXNPdl;ijK z*}WiQ%_qw#T=!Ql#h4`t%C&w}Sm{ONUHZ)~D~(V^p~i><#TZ*g*^IEwF4OpYHc9ZE zslmH(X;y?+K4il{lXcOPz%gL^aIVt*|AUOaF`J@WK_D-$Ek{Lj_5 zaGBbhd93!2ZU&qGU7~WX*IBlE@Is?(TiAnu@CLi(Q|0GNWjRjl&W9Z~$jzyV;Kk@C zPoK@+&=@WpS=xTe16BUOUTi}~F*iB`GRMuIFLAak6> z5wrG|6~ENkB|2IqOWA2Ezs`?j`cV-{zssRH7Uj$r%Fah0Dh{53SY1X+p-(u-ecIy)V%CmVS?3zTpl-hV zxF{hOXg8hsHzt?w>Ln*^g)R9<|2A5YJXv@{7qISbAa9mO>hlterZ4@t*YywM+R@w0 z&IG)iyZW2_XZS)ij%t=?C%~NLr*ytDOi7p!2|h4Z+|M6w$;Um_2er5gLUs)51VY&Nxo#-hs2K8{ow^yRYsQ(D>Dk7R?0P=0@}%*Pizt7mi% z=i_TziIkt%!5*(^dna2M2J&%M!K)H(j;J*Uhc$OZzVwT|JS!MycP|JmcRg& z*JNKi&ByJf7~ofNzZ0V%L}a2vq$CuoVkAgBZ_g&pCa2>#IAKslviq}qpJ0tiI$zw| z3mYDt{pUB%%X_u^X{aTCcsxPyLablJuhfPNTQ!qjM|Ly;D9AabU0Zmk#G3#wku^Pn zL5iJPA@Fwb;h0O(P``-o?_B>Ca3*@w*e92a0i7e3mjG0QfsBy$1?r1q3sn)tci>2PVw6c87(89$GBT#wxC10K{Ggo2#8m zqfuZEQ3ptzDM!6flNz~tLlN-w4L4JtlSnYGXcmNL_mllA2wp23>zrt%CiYY8&vUHI zs|KW4pcx~i$^diPb&6je@t-8WhE8&y#Vy2s6mr{lsKzjy$Zqj46JeRMVwVV~nF~P1 zS~Z_|D?gdIscUwto75=@T4JZPaG^?DQX43N8Tc^DDmN@zT`XUPzR%6G-IZwfTULe1m$$@TpM z&Vn6tL7~YVO1ylCEDRjF)&IFFV*@W9rP+Rqt)Tw{gRWZteNFxs43eRm;yyd=;Xb!{Xv zzJbNGSVqUgr{NNBVXFO3T-GYEc}8eLLub5r%sv>rMKZ3!HuiOGVTr1xnj)?Xpr{Dd zFW)Y}Rjf4!ZX63Vddyj58atueh#rwld<#3{WPOhHVu-WEAmos#C>Ds5_q^lEHZr_p z=kJ3`N7`+Q`i07a?3CH`?~YPr&i&NC;}z+{*zvn(-FR#{&-z zdyb>A|G{dBwl!7TD!jV+5Jh4}RY_A3v~Z;Z!b&I4=)eP&T@dhH!1R0Z-5{BE)@cJj z=eh6}*WX!6k2B4FUSU`-!42BBXBh8e6@fhUfzxG3?db{9(~m4nf^VXj116hc8DA=x34Zebc>2Ee*3tNZ!3AT{e5j}+oLr1s z^(J^)JLCUzV5-UO{?h;WRde*eFm-*+ved*s5bJ-{ue1E-Wga@eJx><~WZnRKYC(-x*B0AJ*2^GZ=LKMTH5c-Ukj7wp1Gu zBeZX~0t*YBX)~FpaP5O;J6I?zwQ_OdCQFsNtTZCm*MUH}a)8VgZ3S-;-h%r>$;4%@ zZ$>Xrx0{pbq^4`6F!EX>3332XnDE+V(zLfrL@%gs8LTH8+TN)ICu5!vj)#8MF@P2Q zKgzy3D$2LrT2WA>LAnHnk`9Lk=^+M047w4JE@_ZvXa?z$6r{UBx*6&2?(X^?{GB(> z`<`>YbNFXmu33wR`?>C`_ueEcerFY!qnVUPI=2=5)EN0ExUKS%>)5@Kh8*Nesv?RA zu-4Gr=E4ZhHc27bDGw?QZPGX!bIc0Y^k~aw`}Ml)Y)1B``e*1IJH=9IJ+Z%uN)~h} zYGho4KF5*9WZ6TF-uiJwMOrsWiW7UFPLFJw`h*a-@fM`Z+)l=Z`BpOUD)| z1Mx81?>fHHnoQ0V3>4oIXs$WcV=Hjlf2GA1m!KZ7rw|e|HuH&q3J*wk(S`DbW?n6; z0plpU7ZcB;D{S3G_>VP9WV*g(D-#jgV|zFs;$P1R_%CL;d$f9B#s#DHfOX5(`RHDL zT!`@Lar&?V`I`dhXP+k=qYA8ij`%jJ$%E8IU8Xf#pYol9Sf+#&ApCQH~7iApXt8eCN?f7hPP zx+tlt=F}V}GODvB3%q~dhS&lgHB6-7*5z~?HA1Dr^;u8D&FGZXb*D8gI|*im?om)} zv92x&m~~%&XPY}OzVYGJC7;!ntPy^lO_D1 zdXm@8gXoX-9MVmmSnbzGs#G?Iweon=Q5@-)#;!(fWNPfk`vgCCaJ?}xQ;F~^KY|xq z7f_mJ(dr+W)&?75*ZTJ{I3;#5h&FSIeCUJx^5YW*^iH4_;xX zhzxFjyuUJyp;G?>d4og1TGtI9EWInfI4lq9my^EWPztb*054)wd$!+QrWr=kBs61R zj$7UcmSjJgc&;F|KUvC3Rn(x4h{}#ipQlG6a*q>nT&BK-qGV+h!%a;_^dy%|J zopet`=jalo@|b`5jFSb&Hlo~});_Vay%U*_alnxFsS9eCpllk?Hj^+GLZ9GfFL*OU zDFDDYtrZe^FRcHWZPx}ETv@F%{k^N;sMkdY7hlN0gHhvhfId570{@WPb-mp9LDG@D zS7x^{h%F<@&Q>GX0&BJ_^SfO0xHUSto4dR6$TjUzuXiqQUy}C9SFijz*Bi&h;dHqC zhRkBdsBKes<@(}?NYIfT1%t!~gA~lbC&`|K49gB8>^gri=Xw;f+Qa|0Swx{I8T6A* zgio?7s&pzSq7L+^i;GA9rtHBeVqgEJm)liH;9V*Ya}>*Xq1w={Fm7npjhO>G*yfWfP-Ik;8&D2r-p$;YO3)_n0*=|91(T z&a$oloLT&V6G%?q;t_7v52^)K5!`4LPuGJ+^ylU*Ek<&q^9homFK>%itDl=ClKk@9 zaL-_w3-+TK`~;U?YWo0^wrIX&B;N0v@YXD*FS@LLAe>Nv0|49>$|@E=lHzhl&+8po zy>%jis8*wDD@=V2Ir5f8$#)aRN9E!QW+em^cg$;s1HUz17JssUm$lJ>%#h~(urt!bz>8!gcuXdZwJbllZzhO5!oSE_7NF?1z4Ylf1H zBU;b?*X@aav9&J$;HTZ@QAvNC*jl`;PnnCkIjE4hV(6l!Soa}1w}Bio3r%Sh zt-fPlpE4Ng?2IHLjATNr_nnV)R-dmVIV>67-1&$I9e~^Or$ekGR6ozOo=*$2Nq}X@ zrDSf_wUR97JnuQBtp~~f=J5soOOH<%QI+}%8&V$_C6@C2tSEa#j~2g}Nr?AKUC&iKE%bZ zhTmr1_!-A-N$Xzn^&Krw+Ct)sQ9XY+(Ckd?s0axVlj>SG6r!~jDVZ~byKLDSZ-S~K zBqYl6Lk+YQe*^_g7=v-hL1)KSG%tnp*QR6_#Cara`OGa|+mvZ7IyndPM|>S~Oif)0 zmP{$vx93-%2l%%y^+Xgb_=6IMBO&erU)#A@el8_rh5bAsL@jSs-RQjBek?oKV?AH5 z0b+d?C@gdSNejsaH$yKmPj>9dVx{iR@;i`73d|=?^troMU`fcUPL-M`q$!hu%O+D1 zB(=l<&{ncTA2xCR=`$`odCJRYjjM>(nJsXLb?LV>pX_O-V5R2ukhRSTG3{gf$zG4A zqy7@{=qZ6q?;)|I-)_gzwYLerzqzKpJDj%3(@qSwS04ISHbw679A74c;c_u;lu>f= zhj8TtFs$PaHq&dUNYo;f2A`l`TTz}KBrhxS^0yu=nh?RL!`{f!P`!X~sVBu)2|&PM1d538Xw8-N?Lyvqylm5w65)b?O9(H zT~v9r(aCE#J=RZCc_I$R2xVyvSAlc9^(RtpEI8a@`nQq8-}*R(ztlVZS#ZfG?3TJq z5yH6!ctAFLAV$@6v>d7UGgDDi)DxHYxYVaN{*~!*9v$f!j6V6d`Jv;d$Ug59vkA`Y zZ6lGEjiKrE-f)fQ?Nrv1)W&DKGnWxM9iim$JYC-h2j9|0ZGUhb#URXLWMWe8p~HPA zPsz$0lmQIN57201WMyTC=hW=XXX{k=7^n5Z?7#aCN1?5JURiQ^rHLB7%$K0FUC22q z19Er#g4I-b>ib5ona3rebte{1 zn0TYtUuPC>F-3Y+{6*@4HUiQPdmX`;i#~pR`2o}qX_@4cVVHJO7$UpOda$*MTy)7u z+3)7uhOHw=xxpNMV<9{Wm20Y@?PBP{QOt4gzRril)oHT8!Hv|<2wYa4sgX|p_M#gu zA5HUpB34YA8HC1@q%4WQBo+k-x`?Hryq68Mc8D;d5 z%0wwkjYa7R+%9h{)!u((v5QBasiIBxw#+eYBxZfMjQlf4G`}I!+ckrbmeb0uRe`4{VA;VH_+4M7Nnz-I{L1XOSfr&N`E;r7CG}(O)dgLyXu^$Zbig@LQ?x`q$|^54X**S9s|4Rh5PYfPMgg1;oS`@omc|3%E5^Sr4hK~wzV&Y@Bz$q9YVmOWz9C@OQ$v1)_2MXQ z29k`^8U-EMQGaN2$gsjw&YdQSkE6>aJD&W?-!6e4E?PS)WPDsaDk$Gmk^QjWtmgX^ z5}ka{YuR^M#S(`B-iT(!}BEV8zUFO&3J4=>nh0iQHhAd_Z2F5XoYQJ|CgA=8j=MnAg3jSV!tzpYhJ z+n;`V2RDPeI!P&)f@1?NOioH1-HiWOwL@w&^)oHJBpIIwB)FuHhQs7#pIW?KFAW); zhdB1{lfly9Mr752sUq`1$*E(EH)~e)7}Z0%auT#{%~wC`5mPtrFH}N4PI${V;~uSe=UqvSgc3UQ`X23o zbyHLSe7$LfB(B#MQY;Z1r=4(V7hy^Hzsgs((-N1_VAN{t1J632uxxp~12b+4RHkemY@d1&ZhK!$|1hE4Wl~j+r+s^K8PGx3X3M0?? zz#T|@1uCgghW^O0L7pZ&urRt%dY~WeGlre6X`I^3Io;mtNn3$~lkMlO4UXi(+9fF3 z{pvr~=&f@rPS`TDN0e@P)`~1@6cN{a*P&$&7+v8XxYI;$qb|Ea1uM_PYm7_3#pM_= zTVFNwn2bPTBmF`UKdA1GZ2k7(o1Yr&9&FJWHH$cj3})@Nek>hZ%9S+TKdC!W5sEb8 z$<(i3e^;0LuJXY&rwid=%4F!UDzPy?gHt#&wQ6(WE3Kgt*vPYhv%#I5J(MErP@T^Y zIvN?dqu(T&;gS(hEW9hZ;v9vZ&{%(_Ln^dTCWm^Uo_n)CKnnWuQl`vZlv7Q$VfdA9 zu{KK-m=ztsNi$I2MZWm?GFR#luy#zj*p(-zvaUHt2O}5bynR0|w_S}=iWAdV)1FN6 z=&I4Va#iS`rG$Of>dTHaDPh>m(X%h;ED&9se(?5(rI)27N;v)PS|)UE+IQGQT@nH1 zZ4`e=<-{Y`y!uJTRc3O(&5dHBY-GJhIC39_vGt6))g(;!hui^w*Z}(uE=9}r8EE}Yw4tO zA&4y_?N9`oKE7P$DUVOR4nsJM2@7fYwN+F-=KuoYT)@OwKbqj5yu3%z9tl z2u{k-OnYStih#K=5y1+iyVC^E^NA#J>#=ZEN2?WJM`~)FxTLpvm`SkkzZBH`Gc~w; z){8U{0p+O(Hz+p1V~O3KkO4Uv=O0hoB61pqN@1-J5ud4*T3Pm)4_c}~mJpQ*!K!t7 z90}ZjXz98NJ&~v5EGGzQZA~)}bfRa^ShH-}Ew`(NNV;Kx9XK4<&7PMv4m$cYfmJ=+G^%7MK5FCqp zufj#6RGBqZcmCqp4K2gCl24>MBF$KiL%}GqBTkc+*ZTEj5eaYkZXFyXGsIcE#-{3* zomwQ%RX)U7d1n}weA2qJPU8Byzjp0}8I&CJ&R*p?O_R;T@#90Pyo4HL)FB7zHE+Gd zuDWbPLW`H})hvd*OlLnT3DUEwC|Ha2NA{ow}I# zLsKl2lkk3I&|KvriGyXTEXq50^SqbEkyxEB=`w^HG2+B63%ZLX;B?h2Qu!Y zBc}Eo1n06Fcsi#*X*#L`*XX5p>gg!AonM)%n` zxj!$IsnK|V8*@PR6Izp`NmH2_$(c~Dvw_Dy+$K*S!gRT_H^|QXi?ay6AK{fNd!zdK3vnM?OgBTAH zK~m5?rx~tH51qJLxYb*q2m!&n&VD%;G#dTGHkM!Wv5|oN`ofsJs@&Gmr>F0*&PT|- zt@e~H_04*<&|&<3sRtKCxRJC+n`XhGEa6VvU0JgujXwa&HoP^Z`$T-XgZA%epnsH< z5l}Z#$ay$}^I;dgeXXEHdI!rY1L)JbM+dos9d9XYjVU5_;fwQgIFGz|?h80SD2gFk zeo<(N%WNnMx+zV?SmttO0hP|BTWzswM-!3TP}386`P&BQN-=vIeALy|4Y1O4gNXU! z$t-{fS+d;^g0!>W;~iC5EqHasEJIMkqY}L*HM)Y-M+b~IvxkiqL^FEFy+(rYl&rsN z-MFzCFfe}G@BOsxMnkG&%h8aLD>51AO&XTA>*@{1_cdjEq5BCNX6rqEFe`c#<%X!e^Y{C0iZ%5i}|r};Db=DfajsUMqV z_MRtY&(fMG1pajSx_xqC%o1Joc6ZdlL4T%>+bl84GvE3Ol>9F1fwSMc)x~;1R44#-`)*0Swgr? z8kBEsAqh2_h|Nj%sW5$QS>!{{z@3oKBdx-z3h23f%*c>avJb#|zK38-&Y6#4sp!+8 z{K@iJWRHa-k*=oLs+QR2rIbC|m$;ctnXkLqUzlWgT6nD9@3Qd>#0z3#bcMYzb>mLf zKA>%H{iOeH8Z|hWXH?E7&I4}M{oaaVQbG8HLh5Cg-P_epPM%Oid+R!R)m22yK4s;M zH?ZGAKaEq2jn!_}vLDsKevLdzi`JqC=;tU5N^oLv74GK#4-F6?iKtN-^lj}0#(Z|Q zf~FI#$O5RKJ$yuJ(=WQfqsAg-7-a9;Jt% zhG;*mRX126f%z|^p^v&Pi3T8GcEQ61kGTBeH$kU>^4VPB5u7j~e+=AvxR4M>da#oC z>fHIj*|pgC)rYn`@gY@{z0dQl@m%MhCsBmfOonO&zF`?#yvlfx8AtIh{n_}@fV&VA zELsdy1@71uC9MsJhH@+BL=mSfSFZ($pW982)g;aNO~{-(w|{mmwG$GKy&@J)JmWjO z{(;f*1|5{QuRC%KqD}f7CMy6TuuECvgwmcaI0&m$#UY&b?;z}6kWEQ2Yt)W^AhkR_TT56@xc_6 z0>8duqD558sFVs4M_s(N20yv_E^!PHYGQB=MFsp6V0M2y=e=cH?EK~BC;~%gYU&Ak zM7ue1re?{9q~RTuF+g-}wDN4-E-o6Hhgm2-m$rOqqlG_O0BMT9U{oo$Nv{aQ)m*d) z;{QJN5FR^K^egQB5pK~J(U&4Gbh zJ|it#>dz8PcRQR8dxd-sB3z2reMfks4(K+oBb8z<1wX@oeCJ=MCJ&Ke0tu4YV8ZypX0?Z{@tugA zT7;GJ@qqL}f?ZCi5@}2`)Wy_Ki z)Efok?IMgozAThFcI@Z-+`3p!)6daPkw5TyXu4kMx1)y`SBa_pJ9mRgaIGRw4{hMlFxMT_&_e0~d0=X>(B)uN+aGUxpBDT!;f6^n5E5psiC z14V7axMjmWTFGe2o5Rha3vKz(2Zk}b!O4t931|g*BVnlW*a`P!7MwuG`RXXqb}S6QdVtR?rWh@$qqa`(y?1blY=!^bvW3*5G3c~nr`lPt5Td3 z`Tarq{G zyF;P)>GfePBkF z-SD+HVPx+GIDa1B`U3=5CC#D6<_u`j<(7Yyt9h-(yU zG-6QEBaqsPix8x?qmbRo(yA~cso_*a#E=lgghU)n5V6=Up~`VtU)3WPjT|uB6)!Q` z=YME5wT{ye)roEXvdvzUu$0kX8+If9?;Cr(B^y#PS#+FFcl7du7(j;K>~N@H&)q3m zN5um4j?%TyKiGdcKv-=d>he4sHJD~Dbbi^GT_l}6Z8XwK+WOqHeEOAV#x62hwe8a1 zPS*|>lgx(}Nv-a;%@4>gHqv84{AwZf^!VY<-k?6|L2vcq2)2a_5yHnHT}T0YBD8b8 z*V}qSRv5}+hE+c-xRvq1{Gq}+QP^WV`Y(2I(r6p#8-%clm)#YkE-P}5rUk=E8K|N? zOj)T7kB2x#HYFhO(ODn3!E%NG$=D00%2x^CceE1Y%<_sew2cH6kG49wV#bAgl@Be< zf1}G^DQQnx{6Lmpit4&nnd(rl9e<)Kqzp2tqnENZ8ipQH;~0wmQ9W}OCmC6^TRA!eSvB7Jp{Q)?Kop0;^M zxcq8e`Yc_7{6z-`H^1C*pOO$|0H4oYGLi4yl;u;4nkB{ay@4TXk6w!MQ4-pd)b~T! zunuSYC?jpJ|9)y0YPQKF#@Y3(D|)}pnOCNzsK52iny;Iq7Wx)h$_&hW6+9-t(`5s6fF=@u05;creP7r}Qt0 z5Lt!?aH(9xmX!w%E>iw9fuhgaEqA9!2>fB`V(6EjK~VYuqR3Ij6Z0}JxRZ8ns{t?G z@63Way1ZHW4i|NB)6YQqo-t0bvqWv1FB5?^D0?xTVsVutKe&P~vnc8+PfB;@+ZG7i zhLhC$%Z(qi1!rJQg%nIz!WQ%8JC8x$*(im?W2=bsyvX%l;WtdB`|Q=P-bYt_jy@!k z5<<2s69Eh*fpsJ`6pngT(E$CXD{-mJwkpT2j8mZ~Cl^WV5U)$*I2Y8sn3Dw#JNex- zoZ&iAht((j!Gfl3C%wJHylp&6SUzBadfe|xS_K0|upOWWvNheXG)q}ejHiD=hS%6h zPDJVSKwSTj{W!Cfi23MEtvyAG$26zqx33aIZU@s|1(4NTBulMH^%qVVN5&SB^feeE z%)hFPtCLUpZuiL3t$Q>*UfP4~}+BPkYABd0GaLEZV!z*f+Q#i+nR%%*%|? zt_PM7szd*Vbl_PXF^Ie%n)d~m@|V;!y0YF&14S?pSm%GESs5s6hg%)`>UF)qWnCD% zj-^mo-keABR6MttXrvE_hI)R3vB%74xSB`yt8ft~~GQLy;C4#6WkDxnha@5@*Qw0@%CAV-59j!T7F~=20kl;qA7==12d} zjP+7BAX8c#qJBHFJ)42wpEn1mCw1%$8nnn()6eQb%<(T(d^UF?pv$`i?(iQUpyFu? zIx2z)_AVuKmtt)1rR^uG_k@KF$ha(LZ7aIJ(JqLHwLyrgNTO>o9F&GP0iH@1MEyI* z2AH1_#?-*>h}K}ItretKRUFJ%JTNkS$}frt>Bhe~I-bt*>_NlEu_wclvS?@9b0QFF zys13D_$s3Fdl`$JYL!8XC1%a(*aSW5TzqL~x+ci=>gj!dgVhKj*{dp z>5|!#VKIHFYdIN858uYWo+;`qhqJWgfoniJ@dlPZ2nnf`%J?juQ1zVr=I-d zZQXNO_KnnwA9e{jRMzx=g6eY=GzbMoFpbkY5TaD9T!VxRv2)nGn#GCrYHD4EXb5t> zFyKpsL;Ey>y#ArS{i~O(SS=8Kn3ng)WHAYO;r7C57~o00wb9QXS!~|To$vUp#6GVk zUy__FE?(mR@k?*o^d_h~L^;g;!lFuGaAn%M`q$kzbq1FpNzLI+q_ID%wzP$R-;Ttt z_o9%mxEHz2zncN5E?H?bkF{fP%K*owG7V%Zq@JOMt(>1A7zLM#*9 z{F~VAQshms*=A1Q1T0q?eTAQLi#fSi)%P6~9!>}u@jl9#esj=l<=+@IxEvsli$BCEYKir-!nq>Vl01)=2A*|H8 zM|iPT{2k%Nj@G(=59mqMutYWYrPq@IZAl}&6cp5GzHyT5ZyF@_`-ROh#4gS|P%g9X z=Y7y>$dAj4qDAqXv;&v)dCj$Rb6vTWmQ}5g%Y(>(zPaXSLu%~#N>kIOb*E2v9>ZRI z3p*iPs1MO=5tvgiKAS_%+8MSm!;>4FINf=s>HJ0PiuUN>B?SS+pe8GSI225w*`7*x z_0>H4Hkm!xy`mA+8$F`HKl1>$p!7%N380T43znq(Rh8XBvG6T-iaR8zJJR&%VYJVE z<5D~Y3Bd&Cqe}(xqh^k`5K%achbwUIff(By{R}KUMk88ZtUH|4ojNzsB7g==aXcBxvAj;W~m#}u` z%b(>|#=d2F@|%}#I@;;I;FLuj0mp@tb@q2j|2nfqN0`M6rB{qLO-8vE>4a`i_0zD> zO1>Ef2Jm|p4>&oSi&Wf8-rHMf+u(y@pYAz3PDfw@S~M}SQlix2YuQ?^TXRg zj-M^G)lijrC>pWsLo3a{m9F;`R~(L=yreleH#`+>`jjMOj*60ou3?~5pPh^QneVkY zAziQpMXH#G)@NQ{9;Af!T8uVSX@=LW?e9HkDjF8{k@%No?)Ld}?E=M35Z>!243r0id~5D9|jTs2OFSKE?NDw8-VP5Txu{&1~LIcPXlY*gD)*tyhepu^4r z=*o>Z%ie3*rF!Xp(jpN~slR$whvTvJMCaGF=N=vcBzMB| zVO2ZH()+&;p+tm9-VsWC@P@OKJ;r|tefgLt@Vj+n!cxH3-tS;m;ph?sEZieIfm%&Z zgx|-*b^V&E*&?4%K|<+x_K4E}ZwclP;#mgPA{#zvaTVo7JVeEk>m+LKP!aU)q)YImjh^J<{%-NOP>93OWjW&DX&Wk5x(iDQYz$mq z4K|2;-xQF8)goW(yttw}6=Cu&&x0Y@kn_EYkhsnSMV@w*qkB6?ji2;^R|~5=AS8E+ z#NV18tFC-UD$1WTR*=AL;r+Px>YM)}g{bUp+*JDfTdkn}{0O-7X%RsdY!Ym}>F@EeP&tb^JQ1DVj-b@3d z)XDz>Z*z<<=LD}(oZ@*xnD4U|B7inMPn!XfUsWfEwV>BkZW<}vTdO>=5HpKBH43+1 zxJ}0E_8Y4pZ>(1!Us3le$SvD4$NuTfP2QC?k&^Vw?V#iMg_{tOc5=^UDwtJ!ZwIV= z@6Oy%Wu+vD#l%uf_~*D0d>CsQ+&_yR0aVu|kUbf040o!YgoH22V_KBi1&10GWRZoJ zHl~f%N*x@+{EA!LFIS&Rbr>vG90ctL3GB6dOy5dhpWc3C7q+2nZ(ZG(P|#eNikZLX z%1*SqPsjg~9@ws800&94j@SQza(M~-1zHCX6~Gtk*%k=oPAA=iiwepu(tMiHX_^?{ z2gJ_bQ3d!2uX4e@hEgR=ifj*sEkhi-C`kM5b_^nU?$Y<0y(WA%OsM0|qo0-<@_bjA zIlZ2;FFK?0VDr%O-)gH!U$nJW8vGkJ{&7)NoMi{0(W zMT8^XxQjVfe!4?JRC8H~dCUIs80o(?JuI7g0EVF;ssA|Nzge#4eM(G8sE|{)fsa%3 zJE0&m_l5g1R!4b;ts_t17ExD_F{?ndiOh?|D~Lb0uAyBxdn4wBHd8~pZbOn+ZSWK; zox4ktGxf(PH;fI1L{d$&+PE)cq{9JRclz$GLV-BeNy5A~S9ESbZUlD~7WgZ~So5Wz z3n6&t-m&*KV-tW2V3vD3b=-otCr?f|E{%FzClYgwu6e_vd5Zx)NDy-6&B4QIyQ!J; zv#Ac9?O?>Oi&BdwdV7u?-APfFw6xgS^Qj#q8I6c8ggz{@D z#@^1`ci8*vR2Fp71>Rrj3v1Z@#|F%2Htv(|Za&G$?B9YoT`v-cE5L|xZ<)N+3TdXu zwPr;IkyMF4AEOBidqxaM_jSlS(H$)%xlaf8ohA2Z3XPH9XWh56Z}eb~Bc;I+FQ^|H ztPRjCDgq*J;Oj4~X47_8FQ%t0iBN>dyito?gs8i2i@z?f2+g6+0zK`64rGdn=Se{v z5x^3r(=&0sKz7~=NY_s*w*nsbr}v;2;DHbR@xXt3Z%lu6X%mm**VUR>Nr6jrU(&|> z_B4Y#l~5%Fi&P3EVS7CArgXsoXhnUsF^K{mK1yk>ib|l>QlM{!T@dY0^ye!-X(tRY z%}caH8;NBuk0v2Y3Grl$y>R--ugL}&#(%=kas4fW{P@?+<=uESQ0}D(2EKO>d@B5& z1X?6;uJ$ulDrxIY@t0WD6Vo7QM`(GgyPq)tm4wUm@e~dbzbOW_UMPS#s=!r9)Xt^- zCO8!r4REa<$Y+m>={JEcFv23a51w%Bx3wT;5jL_O*J%BZ{b=AlSr3~2c~QXk{>-u; z!07a&3|6~8M;Ua{;ffbe5Rup;E$P`D{0_{oGHpe=-aegM%&OgO-=lOQ$7&`2w&obU z+F}PZ6rj#=rdtHg*2uEl);)r@V7#7)^8eg(X z-hG>R7A6r+<^62fvaB~wx=%Ai`*uucT5z7{e8O_CV?8dH9ei2TUr9r)$as7~F{Hq7 z5Qo^uDRVw#PC1@+!Ll3Oa9;9o0)=KcvC_I{>$kE@84E$V|;1P?Eim9GLYF z{$zTsGKH`L{Oyvazy90pAwaN*qm)R)1r(3cNv+u`ampRbi%sLeE7=#Xx(JnM5>MgJiT|^HE%00a z##pfUhp_;7(8Xo`KM%1m32ga~ley9T6;x8sbUU+Y6pjt2}V2UFNGKgP|t z*uq}I9h&%>OlULxq$!n~b4sk9$>ir&klp6$7?^@-dEuzr4(U z+#-zxGn?o?2grkY+?zGdULuo>z!|8P<+WxN0%g5vaXK9Ygeii9X8en}gxlHNxhO

ru4+cNE`QxW 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≺@@-<7*HgD7vi1FNo@D66F`3zhOD3@i>_8gnP0;)C*)+jNp1G zL~za52~ltEjg1X@US2}e{+Qk*Hl2qT+`mfz+<J&!PkEW@r3Nhw<*<)sX(W|Ut4Lr~ZojU>*w5fuTN2<@=8ThT* z^d6;AR^siL4UC$wRj|}oPb~Y}zO@=loQgkw)O9O%+bv4X6ulMBe4)%D>oZBg`IpJX zb&2g?kSV4t@^QWQUrUlB@pM3?EKjtsF*|? z|L^pIMDTAA^IdvxoS3*Yo%(PO9+*Y|Y$i{;RUK7%i~a(GiHGuBZ24=*@_@f>Da;0o9}xuehPLT9f$7`(ld2bJ&q9M?G@htd zCfiL39S~gDL0U*DCm}V@gWZF=)r;)tx$wnjvoV9^YaxkW@Tnxf-eC8%B#apf5--$= ze2meQIOXO`ZjJf9zzD-vlJT>8;TW~YWq?#c)mc6Omyw?sGZe)*{;{=z<0|92n4FO? z-`tEN|J`Au&xm1e&6)j%&T2L|Bc9v}Ggd~?k^Mkz>-wj}mqdqn=d66a!LJyTibfPs zmd~bUXkmVAK~WF-JxB!`P3aL|9(pwns%pKEU{OYE$)qF5Zgj4*H8x`~RIZjtS? z=z6n>`;Iu}rn(-CPDd zdfcsvKNNVg-5+?gcf%Eh#-^uB&2=xz8x~(bKHRsYQi8=(v=riXNrr@<@*$8p~)JnlE zqF6$_lMrWRooR*-yD>}mvDAWawj}`{)3Sh95E-_r#0T|^RFVvKps@GcQkaRGk5EP8 z*pL{EYTP%9h)IzA-kFIetT3kgK#<(U_H83W)Q5R|-TJ^9Q zLPf^BTvw89+1Be_pIM2s2;&T4UzLgy$z%92pGP^4@X}@#IVc#W6%Mtwtl8w+wNERJ zvQ#tqE8APiIfD481?2gE(L!sZD1e8>0VMft##S6ZI+8k+SR3^Ih=(5x^)?b_hh60J zopC{5gzkB9!$7n~^di(cm!pXOS%cB7g-@=|IO{70}9sm9y6?l$$(}s}L#SYt%fZEA}y6>{IR<(vG0eTLzW#>-E zemB$|zj0U6#*-h995SK?v?orm#rUE8DCj5@BD*x<+-hFH#zkVVRGuKZqjPJ$d`vx!HnmNL+CbpT!y5)wY4D+*eE_bI^swgmC#)<-KO$6d{NoY9U~OSrQn(#isvH!NV)(ttr0I|YH{V*-E z9&Wth^G*5@YsW^{KCt8?CPM91FU^u;tzQ~s8=*RUQEg#7b}eJ9=9b*``r84GYE({4 zIJ_IH^5;MR9X?od$MG*J?BBICl8nBc0o@wI2+#pg5u)N-Uoob{i^HW#>%L%!^ZEe( zx!`QQot_5i-s6)l01mBK=A>diq)GhY$`p>*+F?9!_H{3m)Uc!dVXuAlvi-4j6Lc|c z>Up`~;nvEGG1u&}g#%Ph4@u*-%r7cpn$gZlkrwznC!tl+ZwaqbC@b*UISv#D?rj*n zU$8dYOnI--hD&QV6ZvIDC?df&9}S23^TJbDq+LWN=+FEUixQTr*umvB4I&coRYhpn z`}liLV!>kaFgvsYDkHHbCvl%*g~ic%9#qz-nHkLXVBN7{=RA>k>v0Bf{6JViG$Z zgXf|g7X-*XE`Fx0H587JM)=)Qsok@V`AXZHyvpbNXcKzsIMnd0iH2y(EmGha_Qv*+ zsto(xS5Ye*-~bmcMYquQ8Lpg;Bfvh~Ld4DLv6n8rh{Ka--J6mFH{;w{b3I*I=VO8% zoV$fFogYxZ7Zc~{xS4Dq{3}&)|JTW>10AfqRiN>_$q8#8Ww;q6gNxg3!3ICA{4M9; zKx+gEq4P)ty1|xxvqvjjO;q&u&$p%ug2BA+E?XwI7yDOFjxJAi?z=FzMil}Goi{%j z_r0LYCe}ZkTL5;;Hwy6MDQM`mLHzYy9Y!!3t~UI8Wzsx@2csPz0*7kBDO|?3g`}Lwfrix=5AecN~ zU6Cxkj759L_OTw0Q`(ok9bc?6MFnmeK(#pp))Y^kec$$^?;RbCUaz%P1wR;KOHC2@ zQzEt4W*x66yr*{jqPv@kwtKm3Va3#HMj3|xiI#ff> z)!i_meBKBA4#rmY1=Z@9@n+w7K88Jn59sk#xD&ft@rSGfI4vq-fq-(&&SVrkf1)H+ z5YSYs^};Jf#)B-ycQhZwgSa1PJAX2YVt)`XYQ5&G`$I)SKnmq1oYCJb`cv?t(?w_PArVNd)zM`B0o*a@%<}H+9e{V+z!agGuRG3Ss(bu;>Sr^=MGZ6|GPL`K310#Sd_seCY8vR`N_qLKS%IjTY+rLO}q z!$<#&&nUb6OUFoti>#+NiuAB%a6xW&gS+9(Bwq}w^3}9&8?MB!)Aa!^ME5-O^{(=- z-@l994w(E*##xOS{_Zw)d$qYNrBVIfYAZYi(Q!Ot@tmp{_xvdEv?$UUEdCNGwCmLk z-f0F_aQKV4YAo&3Cr8>Fg+Z0M13t&N3{TPi3{;nBHL~AE`*?cz;i^%u4ZV>%4R`(u z)k>UEVC|5PjXMN_7$SX)_*wZ%J{JX#S-?VbbYZxR72jDO|tF|yivl?uiKk%VrX z8CU%!gz*zqxU9Le7J4x|XIFh}1M^@pnN<7rgg3AVz6Au!Mjb;SgZ>rY;Z2H!2Qb5n zRCsQDGe5(N1oz)@Z7tX|V1%tu#*_wN&H8DM{qaL>@Jn!9cqFO_1s1eeYmK)f$J^wV~Mr&At8v3NmlE z&&;A0CWxsQxyLyPlp%E$?EYeB6oi0^b~G@5+GUDQ>(V^i&-=D+cpMOKo$zj#`>#7; z>A&p+ooxdtrvw^+J^aS9CJ;0){R#gvBnoEZUp~Y!#rqD9j7ivaGZ8urW`71=(y&R}&y{+pC91c32v_HPYGHnag6BY;tPKhT^tA&i%~3$@jXZ znEbZ%5I$Pd@#K7$n3PnO4F`UovAe@@b*TF+cxeEYpTB(3LQ{Y9CIkr$r}O{{CBSZ# zdkx{gq!EuHwzIWepG@K{GqWveyu1wo3~%&3&HqwhHCOrV@#|xb$cPFORWpQ*G0vDV-v)3CmIdI}43cYtFEKCxqZwhp)kxlT zGmh=kvU!<)e4kv%+(qV08D|H;URdFC9dOc#-;VQMpnIvpNyR?E`vR&*CUb^GFMvsS?f_YsuNXUrU|4q2oc@(54*>Rma`c#d}2W z@wVDj&(Ol6K+r?5%%u36+o_)y)9#1dbyquMdn$2_4{*Nn1*VAEL~6_9+nTb=Z*wg= zsU8fMr;mniEC)Pa9d;=Up3=e9zq$_~I97nqbpK=SC+QZSx7rveG2DYT0VfPT-yVQ* zHxoSbxE4U_CM~`POzgKS;k2udcR7y*fN@P#Q0S9Lc&dN{4cuq9aD50~)pFU-3u4<@ z?#4yr3&drxFnsTRmEK5_pr>@p*?z}!bG{c2j~RR1=W%Kbl3Ip_4{#@{#TXlI?%FJT z0+}=WbRieuNR~*+ePMKMRy|iOP3T&^^#t&jaz>-!8+Z5muxnmZo4z3sq{n-6`uT*n zsx;^X%9TP1|2IUf;?#J?E(6bh$n8vCjJBE2~?5}R?TOeracaF1rH&LLJxMf66uZA z5+e@`oDF@Y0=dxBm6*($kTie*bNP4iA5XqTE6t3$FUPy+0^B0y4b2%0Gh-^Mwl8pG zZA+w(Sv%JTr7hKgO_5=OJ<#xd97lOZY!sFzOkGX{reRc@+a}aEaefS}KsywIDXc*@ z0);>#Y0MmnmWcZmH>R7{8irsl0&~{wm+vW|B=L7uwr~5Scp~lI-Z6+Gnh_RVx@6os zv>b<3pbUCYUW!2VuI>a5QHg%@9wOU6jVJx5rRMwZOHDxAy76b`8yUZCDwLl{Dn@NS zfgX!gM^iKSu}I=QeI2(!uih4Ro-@rq2>8AA(B!t4h{t=rBP7ERNu!9hV0p7g;*mc0n>N8`$7>w` zhXGGj+}LtmkIf6vBQnp)hqyH6(Tf%yEF-PJ6}A`LAH0NsmBJA-Y##4#jUIyL@@8*U`cvZ{4^pT;1v-%e#RMoVLQ!u&|64+jT0~(S9iTt- zerzMhy z(#))roIZ4Tz^z(JRI+X)5eEx1E)iV*9D7dk-W;uTd)vZqbA)p~)!t9HIqtswm)jiQ zX}JmMGLN|0JIqrX;gD>*Neb9Q(;LS&Y}D=#de~ojP-d2SI5>Pz0ew^wY*}nyXUgko zN=i!d`~z2mec$nnsYGdR9x9P9KmhrIN&1q-7;bMD8_y{B0$=)M8%}184`#{(^83#m zoe2dpzpik1@5KY2ib0+nLv?FwYYLh}IOtE8H5P37i;nySzsfM_t{LuLO#{l2mxl2} z6%Gaq7WXqmF)kO{^@|cdVsi6J|GGsUK3v+usw$Cysy=J;Wx8|*w%4tOn;69^p8R_iCD)8VOWqL@p7>J|a zCAJGW#9%-458(11jX^F)1HRE<=C))G%$Pm8N~72QXaQ)#L50q8UC{h zsBA7XEuMhLtrhQnUbsc(@aB^~j_Z&HqD4&`5+zBzfoub%a ze}CVjZU~YyW$2>}%Hg-4FKx2M+^fc&oNEsV$Qpy&<@UmLsAJx9MObEgq4HV`k3Eos zhl-Pt8eQ$R7XBJGnEy7Zkv_`(-@W(3?Dt zGfP)#ngo7SdGc(z@af&eCjD+ODD19g)X^QwqrWZ%@qTRQFjfTy`{@SV*6Q_rRAUu* zW2+#OB93GL_(*`P!JZ1-_}&MdCL4Yx>RfKsC{|{){I!{APFeM9$QA*lh~Yv=&z2Ix z0ylKo!c$RcmYyGS%e`Ua&R^@`c8;!0cFSST*4!_cP-R}oWVT!auj$H*jmGX}y4bN9 zEGZ0OcxE{cIk`6!Y$a^N>?+!``5RwxozpN(`Ui*0@so3pdanKySem>n;k6tAEWyL@ z5|a{jq!+X2PEiWqSBFEc&Zj*00+ng6P2F81dRKOX{p zKl#tv{%{<^a{;(+8@w?xs@x%}|HkI**o!2(>v>1|JY9@Ec7SRP2?@&y_|}>RyTv_#e~P-13;-7veYvgjSWGK-y)-Q-`u+CW4y;i9$z|Q zGhAv*WFZNhZ5QK6P2%lb;|z13Mr89F_{F>Mn1M#4=KRqtPPB5$uQgQS(me>szfc!$ z7t^$81D4)a;@Y=)XJ4T& z1R@rb%ctaS)9F46Cqq@%*|>Rbm2h0ELV!2&PHYf-6^@)WtH+5?hvyb7gZn4K0H_ec*^w$otUc{GC;*s`$joVQBdj}aPD%0k#+AVkJr3Dgjm1>b zfmR&k9LBgiC_xILsh8B#{Vg&{&$Zp#Q{~pQm?Q-^a*y*yJWug{jDAU=f_@_3>LouG zH;IY!I(!RU9VlGn8@QdjU}E~_hfEYIE9*G1ii{|`d1JPE;Iu3!b675B+%xi!irm`4 zSF!SdYO(WM896S^k&^8*(93>R`G@zeU~+Fh{NVyiO-q)aFx6XPo|t?_6&_Y6nrS2N zS8fq8R%Q_d?{`Dj$$|Xl;K!oOLK(l9aN^fIhRtR})#zr7eQ_U=(O)sOQZbY(-o7-) z5PG?@yCTD&GK4N4*Ne!5~Gbik2fGw_+L-{eVTzTb;Hx&?Fje$hx)9 zd_DfM^~-uo4+dJ~ewq~~uI8!|y5PkG<9+<;dTsYbFA%ClLBNHS#Zyr|pjL}^GgOyS zv8Qj{?EGLFI!-e+Y(j0i){WTzKp&@^Nb4=fwS)C@h_;6dPz|3S^+AnA!=?Eu9=6h~ zrJxY`pcY;OpSM_VChhd|j^#9LA>-iSxH0VU$T(i>--~rK47hi@KgK$pL7m(5!3nVK zj_c`T%%s@`+E(GWRUR)u4n1wd#gA5!4OfbB*}M7sKam$4+Rksq29+kbN7I1b1!Q(# zzTDlyO|-)8tP}W&>oNV(jiU9yk4^g~@}KiNpd8vRxNxP%)%%*ip;2$a^DQLUeY_Ps zgv91M5_BjJkP-Ao&?~WQO3fEKF?3FD$hF=|EtK~w0aN*sUG|v3-F#ueLqC3uqeC9A zVjtBGnHaGg`rV?*P{R^%*=<6+hlYlZa~`jAD4a(^rd?D`s<2`g_9=|#&iGf&hOSSy z2+Lx)Tn#NOfSV&(U)+3RaYOghT)hRH^1~9CGN+$-aBjO3;vu9gj< zQP%-TXLFnUZQsoW)`%(`OrC)-jEFDP_R}{&cb-DVXTxZoD>qO6?+{>D;U(TH&}ix6 zvM1R|f7#xr@Ps8F9}iH_Eg%gmZ7uFK$ldAd!nWPof?$#$b4>Q4W6CSY`$rB}SO7JmC!HCRleicdg2uRy^(gp$d}41Qfn9l{j5hZoAj zH%tZkkb3B88ULIvp62j(DTS;qfDTX@PLMWv1d|afq(fOW z#Q2nkoLT|iH-c~(c*1(Aj3R7nxz>-OWeS45cd@MWuc2Tto^|lbANh*7v-j;L0Osww zLK|YH8|9PGjn`@8KoA7ApnGD;I(YBJ_4g&(<9s4JIWP}Y^$&0N-Zf88+Z)#_@{1V1 zgp+J$Ko&n(3`}*A^t_mn!nsctLL4?T!5!BYpISi86N5EcvL8`S-NuYRG3Eh?tNECf zrlE>QKn<97Rc5VD@{4yWs2D*U)a-)nyHNFzz+k;~h4L}PWB%5hfSaIv#o14X`5faa zA%0*Py-r70qL|ku;&VHcd%Njs8mea#Qdsyimd(B0zb-EFv1&a_B9{-JeSv>VY>_q7GnZ_&`yPM9IZT(~dyV~q zFt(nYQ-YQ~Pk&*v)Q|7t>(@hXD>|hO^aCRH2+Ivy-vuCKWM@|pK0kZoyjO@uIB{*G zxBIG#2aql}bF?M?_?B6iEbL(S*Ow2?)>8{@*XEoXa2{+7@*Ju?+1p0=b?AuO_Vi0- z+xU&eu%O{nS+C+wW^C}#l+r%CQHaJ2h7)Q~0LdT^fhtz}7LC!a+nhzn&4Kx?5g`FP zDkP+^@$}$@l(7(!GP5E;&t0e!S!f2`8NAdB2FajMYZNNi+gZhn=zg)C+h|w0R#jG= z+>a1CJbYmrv~ovqUfNIJexON!(dcwz`M!Jm{uNml5M)$;hBaR8l&!hxC38qCu(GC< zD11JwD{StQ>*mh@J^!86Dxx~eXx;NF-n&~=@BF%{4`BlAi#@9ro*3;$)}n4ZCoxAA z`+4{m96`U0f(_Yl`_DKjz0(f2O#Zz(a~;m@(<#ZG9q{}M2~{uVsp4_p9fobXPAG@V z=W0+1M$V@8>gha%B(YeAL8z*Qj!DLHfspxhZ7!9=%a`3zQEXNAVDe zti=PzJsak!wYz%5`fW&ai+;Guf#21FBtiD_+Ks?UT2w(;B@MsD_0>QWk=zv_V(e^$PwZ0alQ>9F@C(9Y2c6Tr0LI`em;@4;k7nR?I(J1c9Fzp12N7=u2jZ4k@De6(1R2Vc( z!Im#Kh9^CPHf!Eeg<;mN_Gb?4P$-=q)>&Y0DN4mz-nso82al^I>nwOHnBV2T_*rX8 z_M}^p>pl=FxQNC4$!M9o8nURP%FwolURN?9uGSiXN3)Z7iYv*C{-r7r9kR#}V~%Pv zF6#p$9Qpp<=qz)fQNEcv5MAQup7YQW^K`UTuZz|5Pf{rXjfqUsXrO6P)zSuCIW8nP zIlUqIn4MUOynC6;A&6oSN~P-EbS~%PiPscqJ}-|5tl+lG$Tk@lFA+!)H0E5TiGu!bRxO%-I`Bl3?d*xrNZq0x*A;u-l8YddHR5Odc^*LQvm^ z6%m?%`R+^p)9jv0o;Jo~>S2yEb_Q!Wzz*ZyY>x42F!uX>d)xl#b1s&j%1<^Pi#$nr zg44_Q``A-d;RU&|-|2|Jhsn}H9q6%bz9Cc&P|Jdijh(LYdT*yBM?01+K@Qg)s_Dyo z>fP$ms?*fJztv*-w)&*1_wTkC;PCQ}jATW;sXILNl>4(#(;5|gH0Q5tQY%lLvzwSk zFm6L?to>!bfcV%oOob<$_S__t zh6jaY!skT0xy*#1=wV@n+Q9RdhApK#SUPgX=^2wZt5KYjFI89^5ku_~I!qc#O*5L? zzbfkCPzx8Dzin^OZ*m3i1LAuJF})>~bHogowV#+(N5RZVyFYyW7K+9qK9>i-z~m3_ z8XKzY51shBOdA%&^*k|rR0@6=021&uV}9~xNE0r7ZP&?hCm1fVOju);=_*C~X%#PU zOmZE|`c5nIm?!cFj?|I$oUG;eH;5VWkmi=3LYH<4dcQ1MvMo=~Sga(t*l_uDs6)w{ z0v>uQ)#|m~XX5`^(KF}r;%GOMr$w-YXm2aY zc(A1)wfTt5f}Nu~(Yz?N>N9eI4z-lFVcI9f(6eg9v^^Y?u3hTs8gpLzkWGm}V(&faYw8m0?IZ7#2Mh!27YtP*(dz5i3me3z{g3-@QtER;>w`V)x3uYIn-Q6KIHu~ zh7%FvG2s@eM7;cx_%&?lns77h){JZ->ikh&U39dtKT#9kLXCgPvqP}CA9L_AL18Zm zB+tT(ulQ!%Wp4@!bm@fGu-J6?UhjWG`TkNvT-pPyr*8=2|&<^7T8gDgyzOOy4-8}i5V zh2OoW6>@NiX8 zV{GyKLQm*~UfkSmFPrpT#S3MBR0APl&lJwdCD@uwi?*pAwMNh=NrmJ5 z;%hiE=qW$kZ&=cEa{VQ@!>q0*=|>l z=J^v)diovT@{LukmsyoS$k=g3BGK zKizYB(q$pH{Fg4vRH4o)BMcrcXqdQZmo^zQ%)+iaN>XDz5@Z?r6U`}@IpCJ9x$Srx zoa#H)WVf9XQY^dBp6)T%g#T#h24JUbi@AKA=+xyZEjEwr4 z5Mgezk4OnDYIwu!n_c~KO^Kxwd?(yi+wb;E%Th_@=p0G3ePPnSn z;r8n6WWUkRla%%_+gomK;nD=ZmBv)}>8-^C;nq<1#{S}leQ}mI*QUAiY*}1p2@&v2 zz_g_sD#9+6>fMwVpn+*}>|T0GSU@rEI~cngu>VfBSBvxeBVqGp0d#DqluuSTG635w z;aJ$&hZg@2f|q2AseZ_Zm|{e}qPJZ2qhcYdF1Rb-$b;U>9b4moi%qZOcUu~A2^`qV`i6!+ z@P*Jw?flex2O%GN!R_XS0z?OX{o2t;oYYi2AwM(v&VcFv5OtOfZEkC~Zh_)b+@X}< z?hb|EE=39iio1KT;_mKlZGqzM?%v?;?k*>5pS{=nF+U*lne!gwx^7nu3Wg4>e;h(o z6A!3utCl;&Hv;myb=XV7tKn9D-(=s>vTa;mx|^R19C5gOKmf2xLvE)&+Gh%CRu*^p z`@QQKi8H<^`qZy%E?V(xMrq^sOd`EE5j^;viW28NApdnBks!=-2J;G4A~9Rc;Jr{v zc(!si1s+i-fsph5oZ2<7@_p+3Id6Vnyaprxfr|KaKL_ecp20q1F?A>@p-6?$wt@s_ z=I0&K408mChAIryhTs=x&P;-k>5j6`i&}ds>U05c1}_v}M|f{7N?dp5^C*5&zcxk+ z9%=s}p2h=QB>7z!6E6URr7W)|Cf8r>r0GkwIGYeIjuS>n6(hYJ>Qhf)GO7){uE>{C zZBXhLV)Xu=Dh4KuGgm!hCS<|#B6^Gg#Qa#*HN=2u9Gd1>bm%GpUbX&Se2nJ=o2Vcx z?K)H)M~q!`cQs4m2<0vwDBzOi8kOj&p&IQIGCvhe;*pdT>Z*PJyy&$xc|yqaC@H<< z$m|thO!7Fy2gV{@rI|b?4KJSjx;|{?_$ER*;pfOHRb2M+eP1yC*K&L>AWgHkJ=T8f zMy!$*EHacDND@;7zoxbROyfu_!pw8yJ}?@$1=SD_SBmP4_Bf1$6U z5@(&l9d~ROdj-*DX3LriWZDPUU5XZBB!-hJEGsDDiX$!uvuuKx`jysSq21G$943*Mat4#idZQi|)g+Q%Lcg&3movs} z1cel@8t$)$&HN9NLXjmJXR@&hP2Z)tgWWuF2B|stYzL{qxv|#yd(x5Hm^61U2j(o& zQnNhrNU84jaN0tYAJJk&&3I9&LKBh97zn5IQp>H5or8-|Zvr3r#1Yyeywc1uit?Gn z<9{@BOdTrqkXjS~om~^=H~1xmv82+>!%?LIeBl;b3ozegFI`a+*eSUpwMQwz2V>Zy zk|n-{iNw*x=h9Omcs7P+5pI95En^IhytW*ccwi4b0_48P;W7s~_C;gL(jS0ji2f=q zo|{n8zcg-*f9-XpPq4XfJ0JZM1q{yZjD`NyQ7=r75O zT`uX)q;&C2fW&V0)m|T5bu6efv|x!u}W98I1RXoWgu0_hh=fsSWHgve=TQ>WNUs8)qZ9QuaZ0({-Mw9b{C{k zB0t&m#D9;t#XG|;rnm?aV=k%79$S*=`q{uW!wUcRhHiOf^OdCf9QpEjF6~BwvbAhb{M1r*&q#>E#<*`90p$XL4M;hblwrx9pvHW{C0O7n(&J z8OP9X%SBD5Bb-qQG)S|3*RHwT_ApDPC%u2uKFq&OvEA5zzUZmTD$T+5_&s(-ewZh` z>B;>fFEhIbZ3QVdXS`Fb9+Y5BlTrLE!+F4QxT;f9zBQgo)vuIe5N7k3Zdo`qk40jH zB?*_~{fO$^m1gX+K&4Yc9>)HgGPmM47Z*XCX3zx#1FOk5K1aM1gA0Gv?J=R?s2aHo z$5Xae;a_m%qfC9f3sm>K4RA}2va`U0t8XUZN@nWcJ#9a5?`dqdH6^**_S}@}S!w!&ejsrfL8hsgL%PHlU1p_Zv~O7@ zas~%KtP2aT@l8~tB{Qi z>1@_s(Dz*MSgidaYGK|=ieLRY9H*q|mg~i5U3xFP1~DfSUpm`I$D&ajPG2t6tPfk5 z7^C6=cH9+DyLrN`?{56eCdo!sK_@{0le=S*N2|Ka;oF2daX zssVj8S6L^PTO&Jsp(TO5Hekexx-!$Nz=lU^{I9Ue%K=ZSnSUd(a{!hK{k{3pqL z%EW*S0jH^ty5Yfts#7%H7fRwB5Fcc;uko~&-N^A@X#od-rOHRE`DU;k9?_k`3^;1K zgO!Up2voDAKj^%co{K_dg9IYB(TBQ6)oEll&$;l@&v0i*#<+1iz z_jY^V!)i?k+YPyTu0;k&Xt^&A{&ADygQ^^pSO10DR;pjNjnEu@^ov|qU?8+Wg70`z zFD-x;m)1y@%CZN}i`;V5U3d{}W|{s4jEs{GfGM6OF-(A=}tA;#^kZTsKHQOLXh z%U;sYDVMz_6O=zA_1@n-7ajP%-T7KIHbd8}s}AMf96=bW3eWPg{E-*iiXxV~0(l|?h{+zCh(F)$Awqw?BkG#HiX4I)?T!6Ph z4-&IX_@*Crn;`=qtiC~{keT^iAXDr68t;2UYSPg{|Al56O4{9d1!( zu&sBoV4skpB>1Q~W%72F`%S&6MQ#m$w5?)_HFWXSP`cH1tUbee z`V!sGczBh69&s%n_Wp9g&|Uvtk{R6sc-ic18JVQu`MDqc&~f29;-CAFL=k*PJ1dxa zES6vPxG|KP!>$M))_mXoI0@$KQHhs1r=IT}M|k%t_yuGF83! zT5$Atk~bs1N#W$|eWbbv+z|*TUt{bgGIU)JXChmtYKHBg(6iYY;AVrq7Og68C94~3 zhZ9l7n-jOiHcEbV%%W`)M&!r_B!^yemAJ^OT`AH}{~fyWIr(<5$5%M00V&ni3=vh0 zCTP4ZyndpkuDsdUifQ1>R>+LJ#ZaF}YJpB;Ii=YhPCohPtq>?i>r}-As!6dbw3#Wl zp_{Lu)RKrl-$UJqTw)QML9Iv9l z*_dLIAXY?vNaPLI{#34M!0RN@h6Ps9E9fYj&wR1wT_eTab80OnQ=I3NXzrd0s_GYS-qxSouZtgF2K1zAXrvxlVN1QgT^42CBZ;=H2a`msq8~`D zz}LbJFyuKiWK!~QRL(c(FXE6gwU~$mj{n4W5J-%|d;8w|8Q>zB+alc`BaYp38dNi- z9vf^5zW3tLKlPoiM6XJ_b(U_q$%zm`nZzNX00J1>)XLkX|e4ymICXL>ec|(!N zNfe|{SBWaKk=#p zm$@DXKI;v01G0|j*cc5$<{cl1#aTvtp5y1ia5sH3Kh8|wj~U8INn87(gYpB8m|Udb zM?5^HFTg05+S^>by5mQ1vz|FF$I)4m&tSp|(iQ5*!kj6T(5&Y)6 z#FRCxf*EAO)9&~w|H*yHT>j^{dh)J}Q4y;DH}ky<%)EkTzQye{y8&tBId_k=ZXu#h z*yz-61EKE|AQ_Xnf0*1rGp=^tyGCgzH`Ic(zXxJ$Vg>@BB+Ow5pD))VwBNWk$Ac`= zF2l-}qo=S-nbkqGQz>;I)9Vry$&~r-6P-!-IH8c~rXMycH7YU$v`Hjq9k4x9X9cMh z?DAf$bHCU$CYLA9%*>_1p&=sp**7n*O2@z;*Yi{G?fL4TVy&g6WvvY}w}SU?U<_?> z%hRGKsxR8+M}8sYjo^||YYD>@)MR{hf`hNYGRP}1t{qMv$ypwe+ab4yRd@+%xCzJT zWpm6(g(bCU4;@$5s1kCw22$gCUUmHb6<3NLW5yX_uGks9$&hyxCZ|%m%~`_4>&(#h zNEY_vv0aq)qm?x1D3%(Klv$Uc_&xXgk3vjmxLEp2{V2);Z6w2`V z@H28NS0&8ZM0u~26a60>MLyv)&`*PN=-?)Qo_{Ykxbw+g4{0@9KTH#UZv8<*U908~ z)(Bqsc;Ak@2CIh!A2su@-c;NwLWVHg4qvM=NdETip@5%yi4vV}n$hMsH)<4mpT8bUO`HyPr#A!q)t{udb4)0ty1oBp6Riuqv~*Pl7JxUbW)m- zhp_S;B%BBtj*#D;AUz-fv)oYZ= zI5f=0t?)##48JAxKI!9O$9SBT35m8Gw=@}2nNxm1z03nM-30V1?>4OUmt6AY^g|FgL-1HC2ru2rH(2p^*P+|I^mv>%QEqGa4=RuvNS^E*};=hMJwebE|5K2_4(ps&vhJN zvxR)A5ag8uK$1{m0LE957GHs4prZG)sb>nDk!n!;Kzh4c<0{S)s}*p9fhX-0O#dW}efQg5?Ve1Nm-aQTD$2~Ze`HY;Oj1+8;G{?bw;QeO7X zOEpTK7#mY(p^uj+#;{H?xxBiXYZ>}Y{=Gkj8+toaI!WiYNspHm0nA!lt1dtJ9*N~s zQQP&y%T$K}J3+SCJzZW{3d`&)i(Sp^tCv3Bwr3_@^^J%*Oo;0vb3UAZhe*Rql!q^d zlNqp1ey+&?Gmcl@`Ts)yzW2LrzN(g>A!ewq(>$`+W=2?|e%a-?Fa?L>42T78dSI(Q zu$!wnsPr?Q%B@3BGaf9)i5CR~{ms#c4-G^#pytg2qfhlZ5LK^I{95KGIU^mC6TDKr zQlw-$1F&mAD6G`W!Dv%D7MqSCU%{>xgpSF{hV9>ns#Sk$X$03rKvQXdXfGutTe|4p zT#>Ne(Yt72=bjVt{;ok*xM!!0{x?XxO4lK7U8)A%Rf}MOTvc zVim}JYjJt}uQ=gSDKbveTYWm$n`QV#6)$@kEC3nO{O;hv;^0nm=j1kk@4eYhOugP~ zY4rPnhCH7&B3)$0{=UtmFIU132?7!GW2#?6^m>PcD%bB%6;zYA6P8JAlhOZjq0zsl`}v%6mclu@9(akp&>ez}BN{&a zb;iSY6$!CwPcvu84!R1eDST0F@(V?Sbo7L+Gc-2d@g4MBg>qeaP;zrULLthazaU)< zI*IX(@6H~oADIdpW@bW@rCXSuvZ|+M8rADa_qvbl`D*b>pWZdzV^+kOH-E%yF=6bp zUB1^oZ8&nE{sRZOYem$3(4MYD*(P+q1rgQ|nwNn!AH91CpZ0N`a~`qqDStUwOkAnH z5zDmD88N!%Jfqm2WW_uh*$Hfe8X`TvPP5?4yg}BNPu&W=n)WSX@saotT^Js_$#+56?m)ph2~FCX3fXGkl{mKbSdJgVn#S_X2M@=zzVEYIXg=-cja~OYzPh}0p2rSSxBn%m9ztAmxhFGO zZD`cM6v|M{;x8Hi7(t1xTr0cyNeav!H+sXqaNI{dvsDl@RXboD;7XL1uB)#<6d(yB z53bu{J4$s^LvD+cepTl2^%6gGt%=6Ifxa2x2-;G#`scr#(YM1^GqtM`O;E=xdzo!N z(X0{E9WyKylhqH{PuNaS%gV@vEh_nsg)aSFJu8E@Z4Uo8l~VdY!7wj*=;=RGsT(1O z1lhJA^19$!)uY#bdsKg!xZT+b-EO#9`j^Gcec zE^kS1R838ATQqn+H=YT<)**)B0XgHOH{+sVL;O=RQH|V6oY1PR5f*p!389xN8DTm= z#Q=`bN|67Jk9B$-ZV(H+-%>p34sjza?jdDD&&&v4HvIa%&fXs@SvKLEou0atsLO%R z2eV`JwOt>Ku)1EJ;sar%xQIFI<)3wEGL`{I2GeMKvjN#=xNPR-f*F5jhidjmLD0xu za3nER#+|WAhT8whnnun!QrDZrNMqJ;v)WQ#M>U8Kg`vt>6!-5a(*R1%$ho8<-RcNm za17j%Q_KcO7rsBsj(du=+is${R;!Iu^hS?|iM!`2TWf1B2{FFr_r82~MlIE7K4>Op zB!LU%D`xAG=JAA*r7h9)zy10&l+>hM?*T2wOW9Eg+wn1b-KGoQd;zZ7@G_Uql_OD< zPS}dZ+^J5u}y^dqXVV_V{CSr7?@JD@t=m*ey@<2ugrp z5U^E@;K-}*!|?(4u zd&|Dgo#-@^F}51AolU`uFE*0hWPTnsfO|%=9r;&S9DjJ6D!j822wrr`z5GU{)5#SjutZA)eHA<-5))KmBD4~jnY z+#oTlE4oD7jed-dzqy4!*!>J4iu>6|#d+(RbwL0Hxfer$FrX zQ)hpGg>Z22<(+be0DePT0hjn1DO7?RoYRl?=9Sy7It+??MVK2Y=Q~GDN0H1MIaKU&clK`3yBsc5qWN*9V=TRw|1h}p zg|aP&S(IMj=t~&sO?%0d>#;f^4_Frzd+HM{xuzC6BR81xc`q9}2YMdi+VF<@4s^D4 zL-j;;=3M`A&eYirr$&$}luWAJ;5SW7#9r~}4ZNC~C)G-^D~-=&sdCB^4W-~6=s0z0 z)w;7|qR8qP=-o}PqQ9aU4__$P;tS((|cOSO~Y-$7DJ7o zXRHRG+N1`cJ{Zo{a8DGt#LX?JRedn9>TvHPe&z@cyaJ7UWB_DKe~CkVZ0UFspvrCx zrtW(V9c;~+PRQJW=Jf0#XW9VId$M72jteP`yK29k{XTI!x^~aYtZ{exG%sWuHj&Sp z0!%_x^V~vfguxHpK`7eEC#=ToYjzJtGAF%vXMo7n`F)_e96hrv_%g6;~dI!8W6uPp% z+#V|Qxe^_FymsDX0YZH!6&}y~X6F1Ab+ZD{Ya8}FdBwr@YY}Z*-u;FTbo4$qf&u<( z)F@QZ)62$AaYf4roDS_MyYPyBc@upgwV4>Jit!)%2tK+R=RxBUNA(DsJY|PKK*r=cldo*4b)7Cw%Jnj3rq5 zJ6~zrGu6Xcpr&*)4!J)ze^tC<)%@K|$@`9=(O>s6^c|}2Uj%)Uj>oIY>udNSjSLr# z(L-s%0@q2hiX=|J8z(%U-b3nKgy*EyhUcFj!) z`<2~!QN4*rSHf)J3F^v}$e_Vydde0(ezbjHX!*>`WI}Lz`osDIDX!y8GG{!t4Q{AL zTXZm1MWwUuu%7twHT80|yqL8eJds$zstry-AH`TqcPvze)RAgpzkI$bfTCddvW}ZM z6JHWUkuZVRD#wY$OO+`HK7W)|KJ&XM$Iv8ce*PUVw})!KYW$)%Gm@p4jtGJ5s(r>` z7#ILZ6$;BUQRB$9&}^>~_u{_EF7pB=9$kZx607(cP?*T3h}Ze$EX>gqQrL2{>f#&F zG`+8kk5@5YQ0d&!S7;*a%~CQuHVvSe0t}UnwuGj9|N3%?IikGiER79<>c=|~qdRb0Qn;zU?rq)`}bm3`VK`Bdign%Do?S~!hRS~ij;haz2d#Q4- zSy)4!+Sf8~ustRMs4HF2@<*b%DoeA$v_^8I0lncY#U6|lU^;z<>>v%+d| z$`o{_zB?d@14MtBe8O9DIx^scmW`v9rV!T{wonDB_*uy>s5|RR<4le?I!|M`i*Y=@98nR z!MGchyo4IP?zlIvxvsv?91|Z!aT3gg!7DcpaUBLd@KjDv?7%lgkK_FpNAuoL%*PQm zEd63~Q#t?Oi#tNq%KUt>nR~gZf9SlK5+%q@9PKF*>}z#m2Obrl$)a(pV7&NJgB_&W zR_A_1>-90o;F(tg&=HJAT-=df1Ui+-Vu1qV8!GZGZGH16Wcc+vWaP;kRST-}d=07W zzE|=4++oS9#tH2lPKdmjtyA{J;RES?RgvA$jiwbtg=8{tDaN@RJ?abWIn7$Poa>-( zk^W6B7Y~u2Tp3X}el3W?nUaW~9{>>rJrSk!DqIfbga(*36Q*}26j=8e{8;T%x# zgsJ9ttBS-loglYe^^Khm>RK#N<&Con-!4;mF9-CJW$~eBP2J#mm_AZ3?Oa1!JzzA6 z%WzA^_%~nLKIMj>5yBoe>f1nF5y5GtVi*~g@U<$@r>DTJCg%o7JjHVD1am6e*Z8Pd zZs-OW2^Ne$&o!0^13F9cA2rb}`u5tLn9cP~Uoqu}S$YS^ZMw$RNjgPvn)TwlxjbHe z7*O{onP1){h*&SsFC8mrCEE+{l?F} zM~*e2Q&xLF5?5&MGVFeE$8+f>9O&d43pJ1E$8Qle#ULtn1PE*(nKQ|EWv#o{&iMci zT=KVS!PB)KG!)FPy<%!V7TtDO9wN8NrHqEI_ABCyDxPK?FiX8)vm~}YG7vf<-R z_KBhbp+Xuc;o!;c&K^?GFgLvT|*BSkOTGIDZ zW>~bp!IuLR#ix}$Oond}cxU##FbhOj%%>h$qSkXO2QD##bRo@m9o$!Bko(yBy3v-e;GS@qd zR+4oqyaAkpLPAMlO`c~k8OD+hsOe7_SI zI{LIA8y^WOo5q<; zIp3ZESHQZmLLS~Clwjwq(m3Hf-1cY()gjaWk3Emv94JA2n<5n!|F38S${9a=arOmh zSC`p^J^bJz&t7I3U-+ zuHSvO=xe@voOJQUnXw8r#vdwQz1XLlRq_wpkXU9r^7PepiZcu5q+@3OYm{s>j|mox zdu9{!M67^Z4~V>~^jxW6+)M?82NLz%c(mzxJ?x|$9+*7ZL!CzQY3vd`&kibwP%Wut zdP`0}HHQyQag&XU67{l`mw7}QZEoXTRI1OjWnkq{n8Bw7vHUP)1G11r;P^;@jw#iy zne#L6vwv!#ngZkBsBl=W`Bry=Yh8`b=5@cQHuF74<-)C4ucA5V3p@srYl z1(rihy=zkBoCSonYuNb&{rs!?*`=bhBh@AyB6XdI7+~HtRXG_-M7uVg@gU^(uytBK{M60e4nHW!F zbH^je-P4vzI8`mEyh;6@vw=u|J8>&wx!~UL*6rT<*1#*?6OMf3f_hUL(MGr9He*HSY>~RVrAJb&X4LT<`j7+$<$x(ZRzkU{P#IKd4M@i_I3MLumC zg+PV62CdN~@nHt1?o8Jo<+m+r z^nz@<@xofG%|`=2?5$Tnf12F9+5Ne%oFpslsLqce1yR|8MU^AKlkjDc3QCO&^hGfx zO1wdF&kN2b8j^U=9<+5=G{xYx&YeMI-OJte+f(&MBJHTFJg?>DgpmI3vQVF_5^=cA zOI@K?ZtVc^W2TDFz$(6ycnEUHdjg$dyr!{E0a8RxJ6g%JS2Zw%m`G~^{Es$)w z<(N0$3}uR%zeQ&Fm@w#ZU0!@b?twXYYKhuaaq>?_RNxh{W zwuGDEQ09(gZ&Si?3W?ctbq}6r22%ntZ;W%ph@)1i23IkC%OZ+PK|_Q;@v(*~W8PZed(gS~+VQcD^IX8F zGpKJvqM)4JtFZE6l{f%}oV2W=ct*6ewq!o7Ot0WAxuz7nl2^}!CrPRQuanIOWjz18 zEAzj#18DpgOp<>dkhCfUa3CVu&))EwY!)$&V)ec|U1`3rT084Fg64YDx;3Uz;1^{` zhFyDEAnHiEM8rdv&2WoIs09kd!f@0pULEJk@0i&LPpMe1p&_;^iQnyHv8d{*iK|;i z2~0n=6b6P))Jf4pxUBU7-IHR90^+nds+SSmabG_l828Q_2ky|Fwl@XcR*6V1KiPt2 zw@xcD51k-QDSw{z8D{o^dWJV-P|=3){5chQX83Zt<@|UE!3=Tsn;MtR{McKJ{F@*~ zB`U=gr=67Zh3C)5gJ_hk7sNEbv-g|6c_QRY+!{bp-TUM-pj%R*(+YLh$;;SzW#oE< zSsSe`k*HkOzL`LN+^a_QhM!SQaTEHP6ch!6MyIeLe+<^Zp~cYUq{fvM8WR*)Lb_M` zX?;6de@bx}pw-B94@H6&p1s74mt|G2Otokw&TUW{_9Ju2SBUnb@i?28;2t9Pgn++W z(P{+B@E;!)s!Kf0?+b=z-bI;3JMO06g5oueG?I;Gf678%UO~`7dH8m-?85-pXl9E@ z;8vMz@)Jr2a-DNQe3DHhz=Dcc1J&}SSsi#%Msk=+jDPhKn612$e&xL3Qbu_ zvzldm`fY60d9l;1eC@-YFz~pn`GVAD2rHD4t?15uu*icNu$vw06Z)NiBZ~w>kYc|C zdlz8M4{KqLVVcXuO0-fmF+$EDlrm1KcMFr=+B2!g*-0U+h&$}Pi#+KL13|YDgDQ&o zU1R!47xZpKA|hJwH&upK2ubjYKNAZ zB}Wc8FJrY3-%TXG%8LOvSbJS#BvV9yL)74X=LfaPL5Ies35&{dZRgg!RqYF9sDqZ! zoSrChd{S!}l?ltU@U4n^ebH8Tp$2ZOz3K}qF=;zyA=LbLVfCo_?6wCb`xW3pKdrZ&NBLzZ9a=WS)p-pG~ppyJ=%Ee%>c^ zMKb@HaCOrh{Qmf|Fn5jzH6`LI(O7k&a94KuQyBhme0|7I8jX^UocU>`$3^{VI0>K$ zDql2V=`M{>O5Jw#8cM|(exjk ziaAdZ1OqPS-NH5SOvTpV8?d4gx_2n0ajj zEtUav(HjVxkoCI|TCNmJvIX>l21Lb9>1Wi&2k(Z71Twqni$_5_r?U1km4aXf3s1q_ zKaK>M8xG&6rKvB9yYaW-a~JhPl>YjLVI=!Rj{U078`UX#SOL@bl9?1mVwsH+EAkZB zU)bV|Eh4;Cw{;>7#q*n?>wekrU!hH{BVKFu_@6CP*_1H7q0*yx?XKNtzAwbWfwi03w!dI zXnWb~Zt7S=3I52Y!-M*;p~&Idv@y;jjzsfW#x=HYqUnB6O0t(Z(sz?A>qOOmdBg1#?# zS+$jCk3cWO4QFCPB$Df@qo+DZ3f!6m{g;o>vpQ1A6w#v%4D7ANq5*;q<8wUd)!L}cPQl51D z(Ysq3_Q)0RySZ6bICW?5D%vAV&(u1*&@QDsG){3-{37%>>wT-^6;4(!Le+g}F={Oz zuF88)j4JajCJ1VSIHpM4;Y%qOoRQ2KrLO0-bN zQ@ku0nh$4#Gd3*D8MB#r{@nGAt4*iOmqU8Wtg7ixGleIRMTV^28 z06kyovTwD#b0Nz7B1VjpQ>;H^RliI&)t*G#-)IW-Sik>0ls{g^q*48ngc)z5Gbwc5 z*R){%3Ck8?Z{$_j;w|0iZVa(PDmi~OCxFN9EMI{6 ziKaQSe3DZiYe<>-twM6u3X9A5`;_)x^*2LK@?_yvOaXplYiQ@pv?2_^4=Cn1(b z*Du2(P5f6#L~SI#6=eWOf!&WxPcQ{qTrHA$I9>a1DOvLp)psx^CyIaC!Zf}EBnx6=ZdJLY|3te*Tt z+hAmvhi+m48M+UGN`42j++#X7Sdx2Zip!8p&*)XpYdz;Xx4@>H7u5;sgbB_B+tq>P zGn-RyH}5TvS)G!ndFeCGf*8YF&kmI4qf|l4wvomW$F=gcS(m%0X`K=ovlq{UI>Q~6 ze-+@i0O7w6F{kIE(Dh#bIS{2V!(>2-_fw0uJJ~2c3^-Q4K4b&ddi;J}v>y)5s19mcP#4@h^Vn&cR3$0hA%Rwa_P%uof z7@c{x-CL$L0J80vze~Ux{x-0GoM^*7y96@XOfPxJPf7dTbIp5`rWnVubsRfu(sHtL zA|p4K5X%1nWTP7UlIJ7JNKe&xR+@Z8mIAghFR{XEOF#tUILMJQGNdfrP@>UllZtjN zBSu@7_V>jcUF-<5v}^D8rSSPL=y~z`;*SSanuE-&7wQQz{{-!~zlYQC4FGXa-aGms zFi}k-(fMASs~@Kx4QO%%%)i}E1x5#k3Q?VD8ki|6FY&i;MFPPxe%q-v_T75LR0Fp( zh$DBBpDB@SM<0%2eeFgry2sQVCzvE2`9q#r1N(Aa_F}7V8E37i;`+-ecoI1q5#-xF z;IF9}P*E-Ndi$DN8p2&v-Bx4@cOmp_1&@$9gF$deBHP9qs*DjL4HjXr@vyd92^mmV z$Kv>sU9j#q>I5_MK&wQNR{c7`-%)|^l-;EDaz3dlnLj43NWw!*Di}p9PSKlJnOI)w zIu%Fs-kc>cRxTV+A|7F+p5zjc+vBv5{_da6(rhn)_|{k)i+wv5!*i>hLX zG3x!PjjD4J3j(xFyO5T)3=DrXEygG&x1HWA=^}L7O9MBwllxn2RW|Gh=AbQe-*whn zWgjStEa+n?rg=<9gY~mwt&Tp!SG0tR9w|{8jZw&JtHplGz=L@;I20-@{6JH7Ky~%; zi&Et6cf3WQ1^U9aulx|ad+;YbFlu?+`^ImAL{o>FJFkwa_V5>%Je-*%LOJ{aW#-}X z@5b91c*-mvU|VVLTC^8LD11X5W5N;$?(tSi^hKYt4`MU0l=W%u`bI*tUI;YZltcx2 zVaivl%zuKVhDV&Zz%VvD8IvS~VwuL8_Vo6fMoo~+gxg;ZCC+rrAEfdu-jJIp9txi= zEK(>S~x1 zCm&x*#1S9}KN0vFg;<&!(dT^Dp9Rd9gmB%E*>^ILTKz!*euMT3Rwxr+?I?QpPd#K# z6?L8VlWV)qeM6`YNNXQU3fTV4BJoJI@JU`@hU+DjCl(E;jEJJK%~!cm+q#!*+)iQm zQCImL@y^)*rM^7oS&^CK$E^Y(MP4P5S7kP(cT8VK+|KxVzqKTaJbXBHACO+iGslSO zmw01jJJ6wH@xbr+Qs9fM?VqN2FF5fj>aO>4(~Htq1;^p16HcNi!T0ITTB zY(%*wZiwV>Yms5JrU+9mb2tcUD1zewPUJtWd*!Mu9?_{?jsu7RFrudEfCg2MQL>i1v8vYNq(*zU?Cj*|f zUPT|4lwdaW1H)aG6A*p_m8dG>dX>EZKO+BRW@P50gL(er>x62GBA^jMZf)&Xp@RJj z@yTCdhJ*BAGn>^`!he?g7=^dIc$Q<{Bm`OdpfYddZJ3h4_}w-cgnn>qA~SdZq!sQ+ zfPY$ocH_L1g^np_qVhd|?4TNS`6dL0(oq@?QYW)@zudZ)s09x7qrZO9!qVZ-FiCl{ zQ0Akf3|kHhXRDT*F>KDpL}F0+en_M$6K`1VFdH0QU|v9;!@|_dL!S7l|7P}^26NF; zh_*6sB|%pjf{NhQM}NLGL$_Da#CC6z*Pth5PAeY48n>+@2>P$_@o`s+^rRwCG}6hg z26IDCrDooD4K`%F5c{_KdMqh?blvVfK8f@3wUD;TQ1oC(!BDMQ_#aZcQ@!I<+|DYD zUwJh~i$YFheE0E^sasFD1u;L3M_yz994EqYckK5N8E>ahA;oqb{hDJinS884U#N}N zV?2Fogarm)_uu8bA20llk?9OnM%5nqXpjDFJ6P?MR~0fxmuM|pE2JglX_*)}JZQAv zL~mUAySo%uUG~y`te_-C-QaOr7NirFcSO+5?CbDBPfN=Wg8y~B&12`Ea++}3*tuKL zy%_YrOVgrGkYpVWn-EumggH4BtiTJ$i{V(UP?!cR2#o9`%(*_Hq;j*R}? z8pJcMr~qn1x6>_EKOxd4L+F4FQvjC-rG=2${ z>-Bb@ZG-XhPv$*4nSy^)5>h>YN_sUc?q(61O=oP*%&7*x#Q%vkSfBm~CAQsYi9yqf z340dK(=)%m9j-$I(l5t9F-Fi_KJd3;Pky7NO}1JG$Pg1UR50|pM29Yg{J2b+I`l;@ zmw?ioN|Lq5Ofo4xZpR7ycDVw)$QPiOZIbCHYkK=SY1?fnzpJx?v7Cr;SoUbQ+d-7E zhG~exK5~@GWx<$;Zd>f+deWu7+^W9)0MGlAKEnNIRM*EEwfgKXcD=1WbntK72(qd+ z#c>Y@ZX&_y-E4`@3#Dz{gzGqmd>YL=3-O6lspH@C^PmD3BB0}HG&HSDaf=+6UGEk6 zV*gpYElu_-J=+#xuzRSLV;eKr9o^)3*$*mbCl&m&7>V<|7Ru@k~#4DPhxG?eD7?TyXw(8C(g!0AnpwdZ+g>GXZeIAB(^k+T83UMJKo9QxuihU*tmV$riXow;E zHU&<*^e;Ro?y9)qa^FL{*s^G=B*Rv=mDjLiMf78J*s_>-lY){?)cZs+iB zbL#70>&SBkG-m?lFRb6uh_vUjET1H0Ay&-COq4uc%pJ2XHf9nGu{su){0KN2sWOuN za`;g7HU}Kn{Z+~>o{&v>sD!>M)Vuxm(Y%1TZ3a7#G(mcb+4qgJT4V0#9#T&x->SkK z(KwUIN*VyZvW=L3V=w)>g-4p?Mi6#+@~HzQSe{)P{KlF|zNG7<+A~nLt2`>8;V&*Oe{Gu4tWI|)zKJGJA$(ubbq{WeTWN7eL#+u0nIWz{Tqul5>3KsTuV06q z<9+|0=%wig%ZHWsU~yC!h#Q+#tKhzk;}{`}5Xa+tvIRVLaT1{ll9(uShvMKctJ?{& z2s>(R!GcMP(09QgwX>*=cpkmSTU2dtK6A8j2_thd+YdRSKzqRAv&BAhBEJLxv$&u~ z0GCa_zLv#e6}2~F+MeeBarKr_aYbF$ZW7!zxI^LY?(P=cofK}t37*2;-66OX?k>TD z6mG$TOR(HZcfZ}=xaSW82LI04d#^p$T+hr1{so2Gn%T1v(H#uK2SrGAgwdWnsTxsN zwA71Xk}>b`8QsQ6HQ83Jq=nL&vO0bIA>RF#{ z=9hmB?9AtvH(!u^_v$6V7iP6SSeJz_ZnvW_7%Ya4Oi}Z4;_f1P;Bi*x9_>Non)7{2 z^I6_p-F}xBt4ss4=}0UvNnZ3#op(-u?mBtI-l~h{g{I{T0A%TDIPDMU{1IXvPh8YN7n)tx+*A&xas-#C z8}__K1xgaqT$h!a)plqbPR}y#OO&fOuD;Iif^Mg6BGjz179#c*aML{$ew7ZM0~o&a z;IQDYEspSir*Sx{Mfgy6F;Z*$g_?5@vByLss?O#K%bOGB}ae$&e7UE$sHPCmok;qTEF7&M5DZjOn$#!zR8IYc#4 z5dXEco{J83X3Byjpkbb}x%PwHX6;DadVTHMzHuA*Xs>WQR~{4pqb8ha{)vRy?`3)e z%zk_(lzuxo61g}QB$3F)WZ5Y*?C#;*2Q9N(zz8X*K0?`TtgLCw;8WyO7nj>ss^1O7 z0M%F*q}@)(w3?i;sHA%7dKyszklMO~7fD%KKJJjV`TpSBeu3iY%$+RB-~U@B%oAPZ|BOR!%9CI0R>6h2NG^#ck$KXW*??oOmS9BQILo>oADse<~|k&{=iL@GXq_MJ(GP|uOwpR?AgNBKMF0U*vMnYdG5dunnLM@iPK*dIvrXnHe=H4}Vzx=lI zBwDCUk;BNBg)4x^ew(#S8L=BKg}Fs(4b`O1C&hKeCq60#%1`(liBOM{*-DbZC4r;Q zY2zwq#KiAQW7v$r^KooASK_oCp+#a8h)^IpMtltM`CTbx?T{WXCJDe=c0%-gN?nsy zhnryIcD_gzoBza>N)UMn&-ZFp3KVcDG-S~EpfD6&I+f?)@d*DfjD6bpwG@5Q%|Vl7 z9*|MAvmkgNJThp3uuQ4h2}?35c`tPWE59q>T_B}B+}&*Yq}UmNZa&0bOI=C8+ z{ba?wn@1P?Ee8fWnFh9!jlz=Yb$i7tBC?2@TY8Q##m)R!%_`$w#DxAF0nWSu!0h`H zgiRWh#C?o$p3eIdCNj^VS!twNX@r*5a*C%*FbcjfT9Dsl-K}etF3(cn7yl*EZq_H` z`A7m!xPz=Kj(uaXZv67*UXkv$&j2CUNvfL$VZ6=Uh9=GH z-rZ0SXT0@(4if{B{K^lYYMqwUiLm!k`$5YmnnUez+R?Jk@iGZ$;Q0NCZbG+H>$>GZ zdqNQMF~~N_U9A0CNV4_A|9>BX{{G)7Apd91r)(IA|B}SH&RMd#Kc3u+e^T4tPT1bw zPQd4slO$NSx!LV^RrmL)j>u%@NWa5_0Um=a2AcQuIUp)~ol^gRwwm54Wb|yS%@yX) zpFfX#jQuO{HZ`SRr6p%gLTef4MllphjBj3`jY=GxJ-?CWRe`%F;Nd0 zVd&Ep0vO<~r*qRToR(|-_3TPm#EFDcCM9b$q)C;|;RN~5{meab<6f!^;Mr}yfMzfl zk#I^69noOJCn*=fq0a%5#+L6=GnN6zQ@acR`yr^sDc-B4>UV7heIe;`aMX|T9n(g9 zI4-Hn1KdF&TZ|q9%prjbmB=u~DaL^3WAn2tE!3IYx(0VI*gHxM#rZ?ddb+=uQNMnN z2JI|YYz9K?wII=5uPsN{Od1~M=|@U2NSWy{7i6J-S79!NoAAq#+3f%P{+$7_l|=bW zFIKsr2?-wnn0ve37kCf9kA*KSd_o5wb*P)FqC{;xUmc%G8Y6r@48c^~;*Z-td0pYX zQ1S;mAvo!P8c2djh0+m-)ZULqZTAqfn(K$=-FWcj03`~bgPMJw6&VxeMvs&tS)WOY z6n;Hs}fR8YiKZ$D7)DvmnaK-w=t0);BPdS^Kb(^6&Wj*u|e=oa8+D& zc$wdUiQSDl3Z+OP<>|}j*NuXJAUrnK(JDB=3VSau+YHm_n)}3+gcBUJw=sIir3l@I z8N8-(_fvK!S4%lJ?~JEi;;na4mIH zp%sjt34rsaikpU9Y6naId;5s`z#7w)FqiavpQootLLfE+u_|L*U<#!kKfK7yfGL|i zH{nIwrjJo%bn>v4?&Q{ZWVWjiI|?GIwnZ>yo5wuVHj;dR`YMxfdsWI{Kl?GECfSblne<2%gxUL4mzVED5jqxGA^S63HN@x zAX^mTN=t>#G2DMi(z7sZPCds*O=SU*O6Z+Lz{|IfmTu6cZfj_=46!?MqQhBenKSNM zXldVq!}FyNf4{f86QOjJH;$mX{nqoLA=D6}A)X}XfONlnqaw7+DXP%`C_7gafK&t7 z74qU~5-2EM=Uj1(Q7LtY*xhIhNYtu$x6A+;7SN<_@#%4=N*uzkyfZj)<=7ws2=if6 z(>8~#2y2ouO`|uN_1Q|Alu3h8hp(1vOi|&GkA*(AtOm|M$|!qfg*Kr4hw)~h+mFe%uu=#4dy&^U<832dMOxX$;RviF z%+4e81I$<{(j>Jy;y5@3v36d0=_J^$<(Aiiz6J_zW{U1C0TIGWW;8D1(r zP8+;01@&WUj7w4^x0s02og_QWtVxT9U4I!gid}?VQjE+*oAg1c3Kf`c%H3bEh?x&P zkQ$tx@WwMHdm+8j#c?bVUF_j-tb-&2(Y~1#NXW)e@AwFKn{CM9Z&3od? z+UZ?q*~M_{OzQi?99{yVi^e)K(}OVe+cQxT|DJV9XxOBN58a!CDwfwY;@;yDilQWg z1nt_M&@?6co}qVnKrA)6_IFm12W}JPeQ%H91M)xl!0Di#B1F@DUrfE0QTuF1#9Feh z*vl-K713_Yd^YBx^5`!`XVk|e%Q0er?R@z2eE@^XR)DJ2EWZhfZpgC}O_=BUVK}?s zl%P?V`?NQw_hwrulWz8z0Faku-GBXRJ@L8DH-C1|&KS_RbPsia3D1MX0NEsUG!Na~ zkw!bN7z|R93;~{KoVrV9j}rzm;IsI&^R|yaGuo<&wBTk~D`i=wmRV~18)bOL3_VDI zc*{F~!g;*?Bm{VWh(*NzO0`();`Hb3l$;ij+8OuK5fII0IOdAq3C+>$%JxI8n-YjR z{6#VXX^*V;=r>1ecz`NV%6j9N-UnmTOuC$o-)=dqEzzEwt7Us*TaH=_;bGczenJEf z1%^zQ>I7mJTWm|oZSPjCg~9MO0-4gzA+eV1>La6H_go&hu9jhN1mkD?l2l-pg)GN} zec3Q+4mp<+9Im$)Fj@;lGf=Q8Q44Jcjsl!NJWEK>jdQ`$q>-qVOwWqTq?^unt%^&e zN%1DgU$WC=FCxvI+G7^ch`!j*W6&uZ`!B!q_$b2g2L={KhOU+HTLkn@xK7T}KZ^D6 zst+Nfe=6A+FjvGD*kn4kP%Ar9({c;$Pza>oDTW2be4GrAR0?1?B#fygjen2DK<+h0 zM4;~T%L`zu$`q}Qx-%R~jQloFR-pl;X6!fIrqfi`4wum|F2F-S z+3zND_mBWz3?4Dr*@SH0e+AXRACv!yoc>=Ki&l(P<}v;^KY9H6Pm&GSxJL#1|L|@i z^N+aolgJ;{@Zn3w|^podmB8i0J~ z@=BNXkB*Ftg7Wjm{?GM+q9lu6{kyxnyRB?)msXjir~yuRCs#k&_M3IV^?gm}x^h1( z#qRjORrQY|RRoROquU88t}dk1k?_0EWJpLMWQ=j?QK4by4$&KXlylKmZpk>x3pWg& zf{+}R@RH>F&^UlyV z2vnR;?rNgr{NB%mDbZi7XX0)LDzXW)UH;mmPWS0kMd0*g-Pk`WHkn6R*ltih zB&nZ~OXNk~U*)*uTha`;@6bkSBRF#IeZk;ZM%)oCmrZXv2#O;r=SZ@#YX4U8*112( z2z8Gv___Q=E1z}Va~=K~dpqm;?E``cj=y3@n>@h1jt?h{ri>E~HImQb6ALsDXjp3! zn4)Ka*cBdKRsT5W@~?F_$tQElPa?_k%&r!_)ZCb)iDJcE zFSdX@i=LfrBB=!b$T4%fR*4h1V&KPccp(~9GemK>K{iBG+49zWoRJ^dAidzDhtlHW zHZj5Izr=`~VFpqA*2SIjx5-A>#&HkIStqoK*f@rmc(Z#l3Yp66&4$^F=LM123Ivr( zYJ}i&^I!6LiKrTEIAFE|0!7vCY8o+mFF)wucS6tX_X#mYir1StydCU@fh7}64)b0{>h$R8fwf@#}!gQbPXuO6e&Oj(elo^k1m)Z2>`kz z-EuDbq9z}O_EV1lgcW2uBy?n8w0Eiu`Uy z&={wEe7nCDS{@)$$A0Z*MUn3N9%wU>Vr=Eh_@6W6@fPuUH3u5~qZbi*Q7P?-ErI1T zMv#NV@2b|smV1h)$$+BB4Bm&O&R*)NrlZI)T2aEF?K%5@;=06tb|pGm^8A0LmG=vD z7)gx~eei#h4={+P5sIGw@reU;*R7_0Z*=lF7(?-)WW*K`r<@$Qc}lot&@pjFA8y}U zJG7Qj?lfCLkrHJ@VQe)c=gPJ=0!PQ3*e}v_k232tiIf4#Wa0L-8wnP_pym)u>N4lD z*v+Bj=f_BobuGPHvf&M@el?h_J47i~f$ir!7%cFhv6MC@h#4D`CRAQhJs){esnw!2 zI7yw*Dc63MJGt%@R|BDQx(pGW393f{u_DJ#D6>mMJ)4{&yQ0aQhtrt?9+{WpLsp0B zvq(pF(yK1dvjLAbH++361$Xb%>s`pB+#V>JuYtyO3Lru-;GWdGTN0^U&DPJwMGJJ| z$nS{&Hwkd8BJW9k4(0+~Tj;)UZ|nEq_Z%Z&>OYbS?q|TNP3~kQsRZjIV4A$u>^Y_! zE$2q5dgGn{Oz;=teZ;cNyA9oe^OQ$jah5o5^=$JvB3jg9aBU~5qw03$h&2-{5tc%7 z)B8;nw86i3m>|t9d;Q~&Qpz8|Koyb}>5i|;hDAb=l8VujdFi^l(k)rFRr2dx5DR7)ux;lk>t+^meJr`F(mrI8rr6BZwQonJ3>jr{&yfZ$yUOPAnLSO4KIfN7uCXj_-Ap9Qcn?BtV!$>AL5#Bj zi;iI5OH?n(mc;d9I&=}c5;Q2tl*OX6MAq?7kO7gW=(#vjMLJr(jhLyN5(tWZ6eF}C3zx6a#DoK;8H<_Z|T+C|e;`I4&5k890l8^r_EM+hA4AT;+W-!>CaP^?g zV=U-ANlub6Q!N+7i@NHen`a}m992dq{g>DKD57E1q11H!2sTGthK=B`Gk6OTWs6O2 zDZ=gUny_mJ6~0;^SmJbs%JJ3bFi<5)c04m#{zsjZtVvChP_d4{{`~%MX+}#0ZZv)h zMNl9pNz!?WVbJuKw%N@(r$e^8^0LO02XA+ZmFakizFtIoNM@1WL=q<;m6nxl6bf0NcLFr(X1A0)u5aJ$!D=oE6hd+oCYq2B zefAP0);q>2LT0iz?VZ^3eGzKQoe-w$&U5`$Jp@a#VcZ?eJYEd?ZwA$4j)7G_l~l*e zhI}Y9`K*jv;^xL)pu^n1hi$V(B?9ai+C~zw1>V0GQ6}&@HUCORCt*=>hJl9~=1Yck zyG?m4J|HQT$q~N~bDE@%sUBIDEKP~TiEBiyFiv|vspOo2HLasS#z36VIFG(o@{pjV zdYdq--WcK4z`T}peF6tKW~M$*{5B`-JIfyXhq+ujDFx?QJ_2ic1-lyfM4#}qSlqFq zW5G<4;fB<$h@Lpb0iIQ5C@)bg^L?0|j4V;m?EU)cex1Ydd+zk>fq*Ir8!0}30!LjE zWM+6)-YiqkHX;jfMAV9ZP8t=2=u#C*dA29gPpXG3)*T8Yj^5Nb5*mX%7Si$$v-Tuk zKjb}cCu^!b6H{b+^GVhBb=efsgKNUZmrI785ne;)I~=>l$q4 z>>QR1{y2@m883|#RXivT0q>5Z#6bHPEr|dU5mQGQx=X6r2FHKthBV-g(U;$ZQQm6o zx+Jx0&*8|~wLVAu=zq@YV0;TFYP#Kj=q~c;v7_qWQagrZET>t_Rn`G@8m01U6`q#| z8!P#*q&6Ys$vpEg=e=prMcENM_6--oGc;ic<)QCg0#mbOpB1LmgVrefS}zU#d50sN zO$1NEDuM9#-Tt^jhj`H{qB*xYUl0K&X-|Heujr<~o-XZGu}%hTbG_=`@1*z6d1i|m zccCm#oF)gvP9FI9{*~d@X)b&%LbcnyR?Z`K(&N?0+dIcQOd-TzvqfW}RtDnl8NexU zMhSXvVxI^EM$~Izg_9_u0qMh&py5!Vs>LF{Z2)ZheP(+&F*J)tHI}Jgl{=WMFZI}i z%cN4!L-w$?QtH)NUrLO}DSNdM>?@G7?ReY6D zl&+Ru{)MpNX4|Hl8LEP?l}x8uhw&Q^-ssu^D@pFS1%PUm!fu?B$s6%+fY^lPE_zIh)i;fG8`K=Sr{0Y@yK!T z8x7FtA;Ji~c)L$jE@wM%KEs#2Z8*Mm^tsMV)yYC2pGCx=oU0EfZDZ{ujK@+pag%pC z5#y7gD=!jQ(_yBg&`IX?+nE8ht)_~H8R-DC5vjFJWL13O(1<=RyIVDh(O$3Jg`ev8Bbk!vYEL6cABa)uH?w>n;o@z5< zg>oz)0Z@v8Kr~XY8Lm5Q<@$tx5-tuWtCP4z93rgvJ6QK=0ScJY^G}}l-Qi5~Kky7y z)T;q3Iqr!Tk39zZf z(C#GQ1AZ9(U_4xkWo4vS78qBhuW}5Qgo!M^7+~qjY4fac)1(Z(Dk*H4gitu*%*&3f z@;3J74;>_&yRThTM>cz{c>%`Q%RuF9M9yQvzl7)D%L9mPW|D6+6@ce+*Y?Yq@>zx5 zqTW5XkovpTx22crQw}K;Hq@p=RmWvcfTQ|!WyUSaUiAEa2j*viY9(;G)E*iRSfNw7 zt@Ivc0oY~kvhhOOdIe-JfHpZeKWbI?BeiMF` z-T6kHD?`ijqKA)mx0jb~Jln&x81bhPFQDxf zd;Rj~bkC-~B0(_&UPapMQfXu2-P`e5Nl71Olm?5PeK+&7Ft`$8D5RXPHAQg{uR%O* z(?W4OMl?hQiHRj_`KGELS?v>Ux4C5_oZlv@IbFn_raCiFMF>~n??^Xs=qNM3s4}J+ zYt9m$Kgcal|1DijekHXEZ(oE|ipG*_iM9H@s$!cFQ8Oq+*)NhrzT+m_u--uuvHjSZ z-p`9mPz`~!KXzrV0}D|_FJ66usM?Uq(+!Q*XME%Cio+_NNp^b3Vv%jja)K8upEprt zP@Rz*8BbT&H8ktn{EtxmaT7AhiVJEke#t1L=1b;#mGX3)RtZc;9`KRC9Dg1@ z68BYbyA6;R_mPGpKK#rFS+xW!dA+d>ZNefaZ*{ZauHA|?>DgDrby5#s*urz3M>biA zDd$Q<sx-n=W2grp@t{2sK*m zU6j32?eOZD^9ZI7Y-RDZpLRFKKYF`g^L7chAZKa;Ic%R)r*f9QGAx`XzTKizGl~dU zB9LY3M?Tw}_stxh^;NW^5pbxD1twx4r@k-sg_kUYci|U^Tf>6m zt+ndcqmv2uG#naBqbu8*C!BZLoSUOrvyl?K1oBAnl(q_8)@w8=P{fEx?rDsL0^@1;HHF9mNR+;ir_syQ3N0uoc6UmFTr_J^C z&T#$oTkp$DfAq%Pqo{PEh~fo)9^(Z(&E}yHvpHJ(4V$D*-q)e`TI&5ucr8A#_qDX0 z#RCI9AJL@13t+Vw@=0s*g*RB;?E6(OPSH7M@ZX+Cr^HghI{C(i{caX|f=7SstMm#VCKU-M17U9a)=JI$<%w{yO#%*~hgVb-x zS(lC1#~1o+^7}R%+aQRq;geP^X#Efkp&Kses(}2GrLBP+uq0S@Qr3O zuADL&6u6T6IugB|U<-s!##v%W2Lo9~t1vbHut>J@8+!8>O;Iyh&?4uUxCfNxRE4q* zR;r3~YR*3%m;khoDY3}zU)`;6gUPJbbSSu;cpA$hUF;?~n$qSYbQv3{S}>XNS*7K| z_Ya?>BZD1pyo&7346Nd#3muzSdRy9AFttC-kXC-LX&{}XJ*4_#M7T7yfqs_Q?2tdS za51sM>;+cx-W<@RCJGQ0#qXp>IMepqhpW6urQI*Dl?XH-^a^5ypM_EK7KFs45}Ny` zC5B$N^1eF+2p>hg3dK>pcyVMEJ-hg*H93kshiOYF%xhE>ip%EHxQn*wq^8XM@c5u8 zP$uHEbx+IN?uxqio{tu?G|-5e5pkD1g_Cj+r4rHa-2W<)@f1=sE_G2(k*}o1QEq4@ z)Xcg>ytqN>|EtBoYHBlTd;x5B-^|FIqZxi!<}}Jywz$o*+QVl}ESl4Wl&=DbV&eo` z$jAb8%5=>>8KhnrJPEgaAfJ3&!+whfrRoV+V!M}Xn>{rT!Rlxv6QmY3ySW$I8bPPBz4)+GVf?F=7h;<)Ve`nhwhmj0xG0#~qDOm6dX zgMbRy>V!n|lqU2o(RXCV4{h&PZv+Plvwl$3ZRCA0IQAI&N16E^3ve1Lbt#F0<#UQ- z{rN0-=oDqQ<-D3s0$6^GlWh8|^L%I=ljr=b;AlY}n|wVwMd$d3H^)&vr>;)K`901t z_N#C4*p_UBz^HH#2wFFq@2x&u?0~{KVlui_e^=fe_13?a;v{CjULAT3eK3CzlDt?o zak##`r081N-*FZBCdT#0oMD^1X%Iv8e-`7t3VB8b*XnVZF>*tTu;PI$>x#ctY^shT z@zYAiBb}9I76QnlwGzXpXnh*vlPyU^!Gq+ve5Xv|niF%k#}Z6$-$a`4H2ER}hScq) zZi4Zu^%A0s7)~2L8*xn6I)b82Moi++uO5-r(_2QXPqj0^c!}e{hCi9-6#)TsQMPq* z^p-4X@o^N~<%{58i^P4A?pMxhlQ_zY8pO|&r!{ROjjKThq6t=+F zoy)vQTl`wZo%dQ8LajDQx#|EM34RH!NOON3kD+lcz4|z&S|h8f$kVt0yc{oq&W^Tx zxKOSfG8|Yg)83KU;nJ$&*sA`i9Sc%h9_mgd%iH++{0qte5I`F-1f2>I#sl_RQZn#n z)}QhDEc%jiBVO|xnv0T8K`EH7>$WHelq~02%Ht%oRwU(1O3F=8ER12AYU;zmdzHJd zZk0P;Yg!4KL_(w;_nQ+|L0WdZzAC?7#<=n%7y5=h3F^)0;N4m1j@Kljf$SD@zUw@_ zsKuOoyFGxl%NrTu@Ka~R3&S?7hnX$f>Y)G>FjDLGAqwAKo{ShmXW`fCsw28#pU=H)}V*>u41W7QN8OUv;SgBXkglX~Ko3O&K7ne#E_q{Y-lbo4t>) zTf?~}8O5VRGAn_iWOfG+GvG4;9-OgQwcHs5v@?F{@Q>26aPKDP8 z<+>u=R;M>pUUgM}d<0Z_sy6OdGt;#*c@Q#SJ&WFVO&)g8o9L)r>Virz1#Up`&>Cd+ zWg`AycUq@<0(GwmDSRx;DB`S=gPC0G>$U5eI@J15bhA|C82Q7m@R$?w{p;O~HkGYE zZn#?d?&nnPA@W=YR&EmseoIoF@=uhuim3W1l8^qs8U1f>IsFs!B2(=to50G zlmhlrTsX_<#C)QWs%A-~XbP|usi2o)M}hYO-Fo@#aDe8s+HR+w*g~R2P*x9b!oxng zSKFDVXF*Li(9pF8aJqx5M(*Ii%Mufg(D`9xIQo;2vROi{+=pPYWFj^p8;A%C|F`(R zJrHZAm^B$rf$W0)J8=x&>lhl5*>IdwHFM{k_YQ$U#8G(o#276f34O+ten(eBtVEP zS3mUGc6sl&Ppr+8&|U*oN!zSg^6AaWbC>V?%-1Q}L5XM4SezNAhfM1~)*8p(kD`=CWU37_CCIZ*`#)x%aJaa358dRP4W37T z*|Sy3a-l1iOnH^a^y4rqbO_~iLiIsEFfgb$tu1}M0*>c6+hYtjy+xyEd9AuT-yHFn z4TdGn7{yPqouBqfL&%H_V=R4MX<~M&myJd>>25MAiSd7Ci#9DMx5W0+MZ{**4Sb$m zKGQlj%)L&$8xnypWrZX>=B@;+jUxH&ExVgN%)*sKwe>eA2BVd!I+oA2#FjYnV!+wt zxvM%d+-GcY2Nln&F6gYmFR1QGR2&$tVS^iEajRBU6>QHSwkJD1Z@;U5I24PJ@tV#f zwyO!zdu0U{1z^|uIIhMvI!~55u3x`-Ex*W8E>q&H>8Z}zLTn%^@gnmngYFJb>)@^1 z;1bJch6*mUQ!;bAb3Vx^feSEO#^yc-M7|*pNR;Wz|FP73+i@Jan13a|HT-d=SY2-9 zS^hf^`s>R=yh)s_Sz3=_+n{nLpa0x@)TBUydigvQw{CQk^{>GwC9x7>O{mY$$WQcb zY0$gq&W?n4!}#EyFJ_R>$Y=E`g zT#xCz=DXyD^-ZT`>LNRAd|CR>KaF_r1hQYAbNW>e zGT602VUo9#x90tzJ$@ZZo^zCg|6Dq-ptKQ%&Jw@=<_2cvC7MXuwr;qp8S`3`YD|rV zcLC#9Ng5@SHCm%qiL;jCK~CN9s1|tdK;e_O{bvAY`6r8|TO#F5<49Mowm_WjRHDn3 zsa$U>o8nC5i_O&~OZ%ZNS=8LEZL$kPGd#gu8lF9 z^E6ruM-&1IY_t)p@12eRj-v~&`RjPQRUUA}61D+;Psp1kl1m{g{;Pq>zLg4KRn4&9HH*wDIQi& zz5eub+dxCLkSvqQRw$DJ;o#VLSjF<07qdAefY|nYC+*cSa58w-VF<6?t65Zp4g$}z zoMJz3m`J~AV<}~Zu*Syi!{EWfPmmL7GLLY+xYg7hSi}DdQh0Aj8T|nz-jT2iot`!} z!+GIyeeG0cI?+De=t5#G2BGW)eG&bf90)VRb6y`pS`Oc0k_{#+Ye>132yBhn;%*6a z7sobnVFrPJQ4?N^Ezr?m5w1`>&-8ioGU!Mtp9%@n!p$ZZP z2DIAsHp1AQQ4aJ#ejGi!gh?Tfm>*P}tsq3=;o{X$_u{coGhr}wo>6GFXyX?YO6KKVpjRWZYESrA#5!xa0nXfK7 zP?I?NQD)r74PeP+F;YC;3hu|VNX;!A@EiMVE-_shiq#BP-BRr<#}xVMTT;zP6u5k5 zpgKOq%IiSihA7`wPzf{+ayRRjSKtiQ;k2G>s*UPU2nX~uXyLY`xKT&RtB?Htr1=>2|mW3y)6nB=U-0!S2^oQ z+s<;IUS=g+h0yXzza4Po)7?tfhc?3p#))?Gzps%Z@6{Uw%a2h$^0_R+*d+gb>TWW_|{ zAWZvVWmmVxzHZcubn#ZNXgk-cfVZMZpF`ggPCY1x%bp&2Y@U96Q|Zd17hI9>rXKhf zYk)uaC$Q9uu2!l?Yt`fG3+qB$*mjZq?K;f~IOJWoOB+uwmiuex;_ZD-6PXh6v@jz< z<~_W9i^IaG1GOW{nB@o{TII@XqQTL2w#=K1iIXcHV!P}8d-vFp47_$bE*Yha5X!Q! z#!VYw)?}}nU^1gy^aBz>5r5Be0~;XTS+g=E94w-2JFy5}?OA+IKgU@{U#t6-NA+iA zdMzJ|b%(p~j5>P@Vs&o)=Ze$mT@4F(OZ$b(>xu|<`pRr4*ZngG)lESOigd|T_fhZH zyx9A!B{zqB`K~gY>kjBLi;Rr?k2pjt<^lI&>2;>d`9}(fvk+Jzqz57=e2g=;?w)IuM(UaWL2Da}K7P>Oai8RrxtFsrx^v!?rV@GY&VlEgf zD6oue2pi@=lM~Zc*VO!O_FnVHTM-O>mSG6nJW9jfU~z@Ntyp8(P`!l%CGvujMh2J& z?)X426Hj)v$*_U#0(Pm}Aa?zh)2bYQFZ$TW>vOwOU)C3JMstjiLE!X~jF4U4kh8zF zP}E4RoW{xLGsbmbIOH_dV4kWaRhDoj416}a!q?+$O|lnzB84UxP1w-0nMOQmcf(AVN!?C1T2vn7umk2CE1Kv#k_lNuf^irOi}Y#^l& zb;PGijeTW$j%5i|q6b3StIBXh6(*V0q`Q@;W3NR#-tEgJ96JFDu=@{|CCX*l^2OH6 zbCEV@FP=crUc4_e-?_A8G%^SWDu3?>Iv~V(ZgaajUpTrXEj@L-U1#Mpb-rrF`{O`&?&{V{W2y_f?3oYx9K=az8;cU2v9w1xb0p*qpbDCC;miNkaHZJ7WnPzEYisaIdS`Qye)`PV0X9+Mw?9=W zQ81!2HMX4*t_>J}J=R=kKkkj|Wi~D{O*Q?!mS^v<=8)->?N^-E&%Gox=IA`r8~}16 zZaH6a>U7>)=UuVnQ+pBGfr2o?{(Y`aEdN*FNqzdS02a&^t{4Fm<}XcZuk(3f?BScZy7i(p*q#s+%yBUAWkot&W& zHD1euT};Wkn!#h_acPc7pYUg$bGiO73*1s?>CG@SJ%LhGplOA~0ZJ@A)LK283{+2Q zfE#Y&g)stxfYhY$OUzZ$PKrorYOH;gKht9#n|{SQj*%KiRL^OUY^TpF-VBW|+K!l(cH-&STo$64=d42B9{9@pX zsIi@l7y-BuG>_gkY+6|=1+U(yV9j=WIT(Ie1JMu*t_^RhW{mQ7qUZ z#7Ng&vEk|`BRA_>yk$d^jiQKZjcpm-0a}b2_)pLhJ-XB%wz{@T#bA+&SrLL8(r7!uunNVw1Rhm%e4wbno2kF za_07|FdO2%+<;gYp+{-uZjkC!?DCX&*0Xfsc?Tq{)2;qZ4;w9=BtGEOcPCUaC6T<# z7HaVA$HP_8cP#5=l4s%CbZFl@Fd)R|*i;+|wYPwsI7e zQvl4@p0-Exsbw%rtj-3*E?eyC&1dn{l*GDCG5_xZy#05qjJEVetB{c`hdx4QMmj#c zaR@TQrvo$UGcYNINyY9lmqY;HYU-XjWv$`M_(a~d8@Rx=pql{#Fqt9y5+wD2w z8+1TsHyG;-Y`-p*p_4F+T6KM03aB4fw#E0c4khguLrCfA!dk_VLR9%?z9l5*%3Fs9 zaA!9R0+qv9GmaUKY7kL@@jq0 zTYpl zGI)=3+7PsycTpJ5i)Q=3{3Hud6FNKn4p@MfX_t)GoHNXs~zYWF{B(I=|}YD^8ykbdt~fUn9wUFbiF<1gBxz+3+p(4T7&E?fnD9 z8t19>1R7ukZYJj15%|HsaCOS?2<8eH3_l!Qd8q0k?5h|@zLe`0o4F48N|AoO`f4Bu z#+;;;b?q@Z+y4g2nDwfMS^ZbIGRW@4xS6GgX_;YWPgi$;t-i&)9vOkcO~bELPm&%7 zd;r_GpN(e9y{Bn&#zf*53ZwO2mEqD#BLS=to^?6~e*2z@jbZkO3TCf6TS3Y6S^k@Y~hG^+VGC*OaF2o)!Dso7X`_EOW z6_585Vm8OLNZU(MBIgn7@zEm6XY&pYuTEsOMbZZ2V_gh}cA|LrXE(QBJZ6QWH&v<) z*Koxbnncb}Hgi;r@J&&5Tvb4ZJ8Kgd;<{TNo%|B(8n^Y7`F*^iDE z8}8z>*BV6IYN=b0C%wu>*veuowEV+}OEF^KlqTL>jaf^T|Ba@S~tS1lyxhkwn%3JL~PIu&N*vg8tPv$z*FPLQ_ix zT3ck5$woASe&ckBRoI2qMxi0?`6!haHMnwbw^n8yRn0|n;#GWM{483q`SMNL)dhebT@nt zp4W5lJ@>o&$3MV&)|zX~F~%H=Tj%Aabi>zIG2X+DgC4|6g+&o+j=FSBwNuF;ToD(mBb z@C{A>;9x|bZmSo5O-+rtxjE|{%wz_iGa{u#bobHI<55my<6IzC`r>7}mCe@s`3Dqe)s(ksvv{EHVI9CkdLcX4CEtB~V{k~!*#|w`ksMTvfG-!`a?^KU zjyT|)^xC%l=afdWoI3t7^5ky1| zgG~NeWb0opFZt!4dViOgm=1=CaU`;t>Hs&)#iz1|BYDGZ3xR>2hM z&j;Qn%JHf)URCfFeqNGp&Z=NWl(wv^P~f)Y$3J4PUA{!Ta;LLuv=Nl}=?316xqRCN zd+7yZjr!7kK&!%{3HWw`!0!&btq{^(pnI%w0hDUkzvuWLmrk@#;Hy8!*l{9MZqO1? zG@c=sJM>NufTT$2SuyAtOZ3}F+U}a#BDOPqm%3X{e==MjEI(R%9eFNuP)w2DcaW0N z(9mSG=H|k*o$xr=|0L=y$di15sI9R$W&F+ z_$-u^WOiQq%n&Y8&30bGd2y~vUZHJl4*~!2VBS>G}_Uw2wCL4F= z(vTRt^qSI0cVXKc`3zhq&alVnTAf06NhWC0SX}CN$b36hEgkMs?WB^hmVxt?7MiZQ zOouh9)IK*d00&{+=wo?>ZFTx7>9S!7t}?nI{szaGcV50>YVL1(5YqfmiA)B|<)r;? zMmiI3;})+AqiuKaOf7k>9oH$1L~J6#dl5@OFD>KFkTw~==IS*Ldxz}F}Kk?7$f3Dh%vj)xbae;*DvSZ%5&{7@iwcI|xecxt+GbNcR-)4M688~VAm5RJ& z>{0sb90&!#QxLj&v*dXsc>hcP0jILEvi#F9LG%6T;B}Yqz36@2)8+Eel>3nAe1RM| z_~};vsR$zjWZ0gEg*$#!@Nj${N#{W8B8kQFtCmc_85*&-Sj?@Luj()k17}}D$P(!{ zhMaD|8qG}3V^mA8N-w;itSD6I9+f>YO*lH{w9cYme+o)y)KYlo&E_<8@R7qrbNn95 z%6dZLwUIP;wrv8G`T;))Y`kOBXx77LRY^msPFg)XL6RITMa9)!gBiAnSaAD?DqBkH zP@F)R`>qwT%*13Z=&a7*1{0xPI(L&&Qn_{iKE~ur3u3|V2>VSXQe|uCMaf=_+I!U$ zaz^2Tj)IUah`_#W9M2b|IPaMA+5AsLi~>=_M@L<~W_94Rlw_^ouN6Jws`S#j5zQx? zw0JbKEWq=Yrr4>4r>9#IiuZGTz^qPypcU$>`7oVR;rCqb%28b~bkn-^v=2G-IlKc} zO2&;w@THOfIU2+!&f8!>PwL)mlx{a%ZfKBIFMLuc7or+Lo{F-Sv7iE4^KPiSYkX!# z3%2V9Z7H}hygF0+asjZ-1oaP_svv8|+P=Tj_X!F}ePWgi4 zGpS$=6@^}ZM|-4b?BPtP^!)4K199~?=0B7>^` zXCMP-)%J1A_IM{JW(|16ZO7cE@3HaW`b_wm!>ht8^<-*w@ZVZ)L+CFX`b@KBWajM_NP$ecO@d~bsCmB#zXBqwKW3Vb*We3Fq} z#qe6&2&BK&D)bPhZRO+i3g&PUOFf zxe@+pjoqPl1)mk`QrPFazMiBzGvyzI-V79td@vOK^f@)Ay>$-oirtH@V3aNymKGBj zN*fgw75l9r{P<*wk)83JlPe&0O?--66;fDS+}(vH3=)Wf#|c?ztj`2N?~StqldmUF z_rgzT)N*NoF+#WSPmjw_p6i9(-op~+voJx0sT^i$bZ1K*drVlpl~T1)rd23UcfL=y zRr2w*S3tfmm;3Wn?U{THUoFYg!_^}Xx4P<@wC6X@>K9fS^Zw5gnxzpQ&Cd~yibC)~ zxm2wgm(gY}dbqMW?3Y0|#x5`Ig!?hg&_^pJwLaZu+G~}m1k0Hg3>fQ9GxTYhu#dmq`t_}zts}`0 z+e1~g@lD5Ft|z^g9jg~nO(wVD1rqoRzo5~y{7-3?_ngK|h9iJ_zjNZoQUf!zw+J|_pA_DNPOxC*(q4ZJyivxQB%aJ#6=}vs4IF3r@cp3Pv62O>14BCV1k~`eft^Si0 zXsLOY$jY1kjs4(7Vs9jV0O{j#NdCUMzIRPuf|q)kUe9->oMW`7W3*l@3*hxdYWYFM zay_O@BOEbmM2XGKO?$S1nkXZjb>oVZx1V$z9jglWNS{tf?T=Rso39QfuECl2g3&~R zbY&XYs#>2J79pqN0OIT^XJ-oZz6mK|#ejhHhMA$}&s+)E=mji@9jGPeA}JWb2{G2B z(`(YAMfms@)@}4hRwcD9emi4}y{^3cOlfqH!y7hL-orT>Q`DfN+9QtzhnHMuB>jy+ z0E^Z8<6UPU=Hg_Rn53UXr)H@o$pTN19S!YhY#%o}6v`dq#9e4X;9>@RexBOb@EK9Z zDK$ia(lJUNzmdts`pPC8n!0o`l~o^*PZ>epXqm+H6uMeIu2JIG5dW||csXW?r9h*N zTrs6sz3_WBwyx|*<%H4Yz>+d+iS-N94%5w|w-|I%|C)y4YF!7z?PhG4(R|(zU60;5zl2%k4Lu~qK^dj{# zZlfWZOfSs9c3I=T-K`yzjr=^jvz8<8QF#79d^gzh4WqQBH#lah?eK|)t>1DH9D zUbMu$91rMIry>tRJk#o<$U|7jvm0lAYIqsE3q@$rFVJfdw)v%U)SN-9+=2dWK801l z1a@}`(Q(SA1=XjSa1yoaSjPo&jI?0npN76^j@@1ZCItDWLTMe32Ksg~G1*=1vpD{hY3zQ2$_ z;YAk=o&>XebIeQ_tf@Q~s8G-lwnk{bwEOX)>GX?rp0yd-vC|+|r(^DcsUA}o)jS@k zS{rlYEu}R|EqAz-3qdz(7elZ>Ev~eplwzshr7`<2X48e&K2(w~Vggv@N!~s{H*7OQ zR{sH)LjMUaIsWc|sxiQth~$BlK6Mt7Au&LDL{Dwr3gi~AE!Lc?-+CJ%sL$MeiegPgo+~<&nsjwOppr1x(Jf zMOm@LBr+2a5FAVRo=c=~m_=K}Sh0wF3458E%Fgm)&v5) z<-roLpI=6XW!oadA`IY~i+ar%OM>65)k;LTw|BqCIwZoPci7Pq7VM@butG*XWsmj8 z>cX?_w0;bHhpC!-qp6@MPBbO$hNwv_l@(HucCfKC=JV_<9BGDzTz;9Xkz64q{r%|cA>X}kOoH@pYt42azVjGcqyH|-3~a?ZvDqi{k1yZyE->VtECbK6Ayh-OhP&^S-y|7WW6sPsFWPqff9x+CMMAZ(55M zWj)Q7YBpetoZCQ}jB=c_n~Oyp$Kr+rhPXZO?=MU2dh2SKt6jA)P%xM220nsT*d%26 z=HF5g_T+y;U`IfvPt^0Aq{q6asLL65|rnsXsy5Dw`zy{kCMJD^Q%!eHnPc{aVC zPX5FI^^ackT;==bk9?#V8?Z%o~nB1B$jG^q+Xi6WZ8Egey=le=n&6&Y%3 zoKtj{n1&u19-Sl+P+Ix1SJP#f;8%<%e{cPq|19MlaBrSch15 zdII}%BNctPo(PjHu@+z8W%f|tr=zEq6Xc(RgI$ES&3;%k@+rH=kGEw{bD?6`inJuU z&2AVEcUOJKt6jrF9S(G4f~WnI;-EDT1;p^TI++&0bIA2hyO{Fne5L~5cRHK^bhqyN zIQkUUc3afenIY&YdwVcn2jJ zG18tQMu4ew@r0Xns`u8a@0J{!qzvyGbdy-^`Ibfed9DY`I4ZQxsz1h!S8^o@SlwT*AVUzJw`Y)VgZsaF?BlG4uRhhx7pyRKB z(X_zi_D11uRSgLqMO_DC{h4hNTMNqj2E%z^rcK2Y13z`S%wEJ*mvJpzkWAR=LEESI zXT)VpJ=(=2ihL`XobK^RJh4)mw8C#uT92>GogOXct(LD1+U~swRJM<$_$D|?Z56vr zBI8FuUTuU`Fn)KMCZ`5kYk7N5ENO;tO3l~?B|2$RUU zn~h(f9tifUQ_J9x)E~fnl1&e8yP3~ixlT1c_2_YB!ofZFmciRsCM@5$!!?ET9IF2S zn+q-eO7(<-gN_5=reWZX$9&^Grba>DeC-Fq24{34$eMq%2%a}~fpQonGjlt6Howh-0fvA6q6=F87EFzT&g25E6C@+=9iN*gSjS9Vy|5DR|9u6M9C* z?nTO@j)yCu&lD?`4xu6;(#oltyjbRfo~u^Tu$)!^ao_MlVI)W2}Ma| zrH~CF*i4zYJ9xX+E&9v8=&aL8kA#+Ts4U>AOfK@k;_6UM(J(8z&4L69GZ6BPvCI$~ zZ2rPIUgxB`@j=zR;@v7U5kk7SV~}dI`cE}msuJ)ejGTiYM4*-2zqPtRw+z~o$<9R- z2mFfV2hWuW>JokjoujLr{^3FE--9*b`9E2>{{Tw+9`D0qoIKFRb0k|5QAveBogY`+ zSA%dP%d0IWsU2nQeh4D+W4q}nniyTfk$M?KYvK;3a#_9h*>?4To3FKw@Dxz`p!`L| z+K}k2_pQ_Y3Y_peQPBW|wj`*a=xAo0h6y6u%+9TY_u6nRLD#Rud`TF##;m@xro}wX zJYAH*lYq3)=G#lNzJE|pc2ovY+GXz}Uz!kq_{Gn!)M&?1NgY@LI9A;Y^JpcR zBxrjoS*^K%D=;>lfUQ$yWYX|Ma&d2xYi-UaF-lZxzThep+%aeR?*Z9y(-KEi2Qr50 zA?K0?`q{n$rAS+8<=~CI;lDyf;(rPir|a4VZ3M$?ZBq?a|Eg4fS?@oUilhC4OzM() zl|cGwSNMTO_2b7>V;pFJuRI3&5Hz5}Pmerq4h!op4|JbUU@*?+HV@+?pD+U?5BU`_ zs8a3CeVD*3j?9Z5>j}u|l{p&~NganS1LJruew;;Tf#;3~RQuZA$T%{4O1G!t8>39( z)7bpO#06bBeQ~%ecAO|m|uK=DtEgG|GhH*MeD8Et0zF^2--Eh+x4r|pgSqKR0WoJ4&4iV+@ zn^&o{hy(O_ip#+0Z@;@d5(KJ98YZ*V6w?$V0V4+`?Pgw*;jelllPDGu8*w@{S=!E@ zQsf&W(b@O7V8td7O?0VXKv5L&C(D-l@7*4rSSv)wY08EZI{KiP4f1t&&L{PR)e{Da z9pU*zo9%x{m8v6vf&ZV^F26a37gw4wP_&;%FE8ahDhkU)#R$`TqsQpB@oK&AT<*)B zF4`i$Zy*YIUha1&_?%)z;&V_lE06Q{N|D;XAXZA$WzawMU8SdTjPEXGMx4->;pRpb_b9%cG zcJ}-f0LUKiqXtgR&(DJF;H5x@}|6H9i6Qi`pAGcu23X{pwLCpKgSo;F?^I4o^DTOb1`zpO8LP zV8r)Jka|zKabwU++W!^^E=1AFN2(jL!dFxz|FBwHR?|S`X6qoC6=0&WS+6aImXGBk zN#vT^jg#fS1JjC4HpW^f#KaF7n|}9$Z6gEfV8tSPtc$eg?J9=*==_ww@v7tZ2Lm)U z;VK;1$-yt_7@}%g-LkH@a@sMSY^MF*^x+HkJ^jE~;F9!U!E16W8|rqiSM8DhT6VA|C8ckF;SoJyR#<38{RM?DS|C$1 ztB8`N>k2NaaQLu_{F1G}k+W9{H zBXtBN<`R`5@j`NST3*Zi&v&`OJsr?VGnA~Ksi*mBN|*Zi=vbxd^k8`3{{W13pbV0&8oey zURo2x*oLmWnPqlW@c;K>0t5-Lj7)!lSog-d3UF@){QIbj0aMmZpP$)_n8%%%a)4#6 zx9d94wrvSI@;>qu*H9iCv;f{t5zm!=9cP1Q?(k0Ci(;si|CIvYOQ~cG;rd#YqGt8k zlEpeJ0sFc7da_QVn#gzGDe#6aGWK6c9+OD0X6Sx6A`cfLdRS%wTbJ;vI$`n)Ek>b? z+K8PL|LPwMB@-6(_pip>h_2I<)-euKSv4(?s4z0ZNRp##|M6BVw6&20XOLhFTQHF0 zWAd0i87)|tdU_pEwm8!TFUK65hvTVwUNK*mvlT&Uo}1)cGUd>!n$gE2dM^NRG*qZP zP*(>L#LXHU4*b($?jzk(8)0?SgXszbzwE~L6$1)6 z+s~bfyXdo2L3}QBb3Ums>>+(Ft$aj-C}lR9(bF46ILzrKTaZETEp9?#j;SA!07c8* zV8w|s_bNvW)}kq@-U;>qihmkyaYz;Lu1oDdG^NOY3u0mK*d$W%oSL7ttXzSsBRfey-ufC8zq}`x$20 zPznULpbR;LO-TzRj>(~ubcP00$}M=hOS^*{$8M|{k;7QBf1@AhRAP%&(-n+kyaV~hQF|D^u2!T#u4M~G9} zg>^M?s!kN8&P;jB@cVnQdHb%eh|u$x{$Y2S;d0mtU$2<93a&+bqMBiDwc?N7xQ5=S+AWTplPow|;V&s!?a|P4CH~g7pD8`YtLoG@E3*O=u?f)}BZh z8UASTNo63iF~Mv_hT;GP<{5Q?OgY@z1wufvel}g#Iw2(h8isB9{%_FWQ;hU{T>iZ% zKu>4m^1Z)1#>zZ*652;k(kb%0n<^hvkdV!P~XOqUYg;1XNTZ*HrfOyuZ$PpfIw7Q z3vamF4SLQp@!!sYoQuK!rjX;U-o#@*Na`Gwp2aTJKntRp9_EfUQV9uko>hs1VVnDI z;YVRI*Ui%vNY+gUJhZMjArG}93;o@CrmT%DF<`1?NB!9H~34 zcB`*>((|g19PVpxS>KdfKKj7;VsLb+TD5Dfp9GrNxy3Orlatk@TsFLI|xU5$HEkptqBVDKvU9Llk8!$ zu%)y!8it)JTZkw*+J6UHdW9{^EUJbGn}>I*EHj)Y{q>ePp%s|Ba1bE(Pu#uH;G!ZO zA7Tf2*AX;1UadmXRLp1RQNJbu{RN$7LAdrlbKGS=cPnaw2pMt;-NBnk6F&A(b`uBk z`QB*<#CLf~3CNd08AW9;Wn|pM3P0y2zJmY#DE`K2@8p1NM}L0vP;CAG{L%q_(09Mg zqita;EDS#Ra}Tweyy?e{g~19)792czHZsieY8+Y+-OB_Hv@lhTgBE!mV0sbR z@z2&rjUTi*M$z&S5`Np2ABbNuADH(nK`^F@&EMY?fxqz31aHP8l?&v&n8oVqPsbBm z3RGUtG6|$UEWn{;po%;`cPREOed$bk&`aJR&tm514-|LOp^gy3O4u11~Tdur@t)8rE?gW(FvwsL2bl*-;TaMB7T2ZxF6v z_VtPnTLlRO6}wdfMX^B!8iyjM{@XRkg1$TlhMsparQ3gP)S0;bkF<2k0im*Qjt_1eE z2>okB@%&HqeJqWTOA7N-N0itS%Tccwc$D!)lzYH+M2~krhzR%LyenD0Em>Y-)aJjI z5Rr=B)RP#cqReHfUArxZZ3^@pg2NBW=MQ|~z%q)*DLfbQpfo>PtQGvTqC77J;Zi+A zqUv5M#zAsrU#`=UC^1f)+c=PnBmHQaXEzMUJBejS*o*;>2hLgxWD<^$h4W*3@WPVR zoxkCOs^)fDv75D5WiAoBDI!$jErWSGnf=^`?MH@Uq^LzBQ3B)_aFvOh`~=c>Y#NCa zn0kJTX)B)DU`20b_S`rtUz5{aeN~B(_bYg*^7+HvETQ}pKq^We^I(DYFTS5={1bVQ zy!-)lngMjQbtM1gD0nA-44l}uJtHh(kGCm&MnZKiWD}?vbGRrMT5A!Z_}x0?!X$m( zT`Q(NwhsWMc=>7m^l{qvg!z1Mfkhzn@qAyW8clR_iQ6b+a|{+cm6lf%jb;hgJ#)%W zG|ABzfP;HM;jh~w^$}Vz&wI;)m|`}6Y{y-^iL=`>t&Z!AnfQeC!)||qZxQhK@idNZ zoSd45XsZok!05q$&4}Jmb@u!$1D>xCzT*8Yv=G5HY9k8xY5^Nb=Z_N%j!oT;ZU>-M zO-(1^dOrzFb*}$XZ8i|(ZdMNKO@y;!bs@sFO7qJRx%r*<;S_?CidW$Bs(iSq_iT0b z1pL7+{WllEeLddBKlMyV<^lEe&j7?s$G-<40`Ok`Eot5Ng7sE_`!EvHEh2rk*5Rii z)9TdSLq*BD`z@as@<)}K?Y|>9vc>*#sL`x1S+UFdiitNe>kQ4fyEy=ldl~UJcRJM- z)~44@>n#|7f~B|v6v4!-hp=Af`V1%wr*S`>CcH}^8{y(28tUq-OnUG3X5XY%%jV}9 z#LGrwDo&9%kP^8P$H`IJIV@65nH}^s^3W^swh{!*5t-}=W~9_Y+%r8v&M=HciYRMEOq>w%3Tl2nSnY%f1$~{Hj`0-_r&d*ur+Re+BNgUDPZ1+ZYs8FC#8o{aGpXuK!vo{J$#|77+6l zHJk+pd9q`mek|b)J&LY@{yWu#O4C_ftUtvIh;3+=jZ@h@NcLP3S)b(u2?>5p=`FRM zu?@4U*q6~#2hH5=+Gco^$$Py48n2lbLR;~>(Ue!>a%S8JF;qBlYwx8TG(|F4tf9Mm za$nFl(t1u~HFu#eWSNWFhtb?bs@9R#E4t&{QA#MCNHM9k)x)mh??ryDwiK9AqZeHb zr-eCEXIgU~w$j%19Mv z`jwzum%_r?%B2;!kD{g1QOzGJ@#!c8BE zIZu-H0}pxb;i#;d*+01@We8qVO-_AZE;QCX{BR@}Wc`LLAe8#bpiZoTA#(|}N6xm*pC zq)kj5Kl&JR+r)B-jR^-Aa-1sv54s|j4MJ&Y5XV270CiP_ZC@rX% zL28WMD881dP;Mb_vDcB#JRK$jt?*oRf98B|ilqwJ2u1u_XJ6{wT}of6Ui zS~VY)iZEN#$6te8>(hUc<#XyhaN_=KfF_`2%|_m34a!ko5Re7@_BVh1QXczSk3L_?jL(SN$Wn%HTUqdfeZ!38Nb&V zB^;ODLG zA;h!M-g45NbL*(5_o_Dg`lhBoV~8s;u#jyd7l)Fi2CspR4RBQcq~&?3qNWA&7Aq%o zL^p=1Y|3zEf|6H#A+lDQ?kY4k2Y6-l3z3o{`2d7LJT)kx9;MT~fE|9l&E;S}>GAg` z;aMNYyYCWUXh=55GD_W+I@#X*K^iTFFl>dOIyvVq=bw)Ie{<$Qs4l=>7~9=1z6LWM zz_Ezs`yrvFbx5}wq=-n(q8ip8*HX#h9e3I+)RaSYZT~VjUVF8^%W3fC*}6O2PmQco zu72n#w^QfVWSYMKM0BJCI(mk9*`c_9;t^;15g${^cLQVq%c*^?BelG(aG?aQKr1LQ z^irH$6<<5T%vXWyoeLi?U*ZU;qAM9nb?IaU=1z5%i;Cxd1*U&N0N?x{&Mde$tx3|Ft&11G1+gnQ(DC6p{owcrP7HYm+L;q|bT z^aP{T8oF-0kOv7x)Oss3&!`#Q2<+za6O-(Q%k&B%eh{?4lJa8l&bM~77-awA!lN1J zc*@{`aC*E0hG$l@!JKJo+0w1Bko*+TK=Ze0VL1loJ7Ja&?>*B(`bwbx0apsLLPCFf zn|Y|@XFp<2J;Z6UY__|jZzaeWcsA`4Y|OwpyPNxJWIjS96)uuy@|&6E$=H33o0kh7 z+6WSmU}Y#!PHWjv4*3_h*qS9sT7MMFtGBiPvijg~-|m00C3My9Uvquq@32tE8XWRz zb0w=mOguMBQjGk^)5`{)R}>>rfB~))R`SC*GqNjPAgR6Nz{e+^$ngK*Gr-6lwD5zvdZ`$plq#(bNTqb z=>NO4%a}&-nj1nV{v8*R^*&+v#hff8aBNNkN>wfc{Wp88#skss(kk9fIpt`OHD?-Q~zUkMi1^uyOXu#9@Mjs!94DHGn zqs_;!mG$X6zW3nZ)qY11Wk-W}ny_5`T1IYxoDUw?ypR7@rJHex{Vt_DkKcxI|97l| zDkC{Q^X{DXCNVi}rgn}9-A#6TnWM`o!neHIdHDBD$;1HlZFgu4lrZU-BZG}Gsm!bDi)}+tkkUhqYra0 z|2r1M3JzhPPx?~0$s>+LbChA;3tDg|s#`tiB6y?PM)=EIK`xjX!A6y3oP?xJ69zY{ zg=7(F?yn-5ysQekZFIGKcGCVA?zNSxkg}Lzo3)r`UJa>ut5=QouzkdnAqV}Mb7fqt zN8@Ieem~SJFrBK*P<=K6qH>FQndJ7Iwh8jmy#6tJNav|{Xv&hd4qd@x$9u|$irp5O zZp!k_;-U(o8y8kGes%KGj7=b=;Z?x4wZs!_v+M7^xu|crV4siQuM_SoKtb8e{EHb3 zUfM3eAF!|rgV6jjp(>nuMMAC#b@yX9B+r)75LST-9wF%^jRun0Y?gPJb6sBGF! zrj5W>=Nzjw=1j>;&0uET3_~O$9BA0mYov9baN~I#jK~jWfyEUr<5s89w=P8coN6G z9^>u++s7~6i$;ob;PVNw?m;jpMh16C+?G0AwzwC&OL)%4RIyFYc0eGZ0FL7NxShazT&F}GK-?fh4)ZGFZ^|ARAo`-0+`GrLwp2j41z5Mc`ZBHjk+ zr8gUJ8m(kM?6mE$-k$UrpV%OK=0oLd`}+OL9O=qCRxyu?d~arg-q@8yOR)A_1AIIu zK`2Pj!FO5w6Qdv{&KIw_ z##;&rm03-w>6HnK%qW_nF)Uw?+&-)-yqs;b{4?3kulK{%fZ`%Im$YLit)2 zw0${%L7t0m+6C=7ketp5e1y5#AS}X>HN_1WdSQs)FBXva(j;54G*W6!0zeN0qMj3 zyeOdTQf7CF-Oa9LHy-Em&n#Wx%5fn_95&usG5hrtaf5E}^N}We=ps#Gn^3+{#LI6d z)6NH9nPk_OW&hov<2Ow3|4bXss{bdgnNPd`-wG_It1f755T;;|vSBo41p|p73c~5s z;j_NTxii~xJ=@85?)!;`S zQ=qz1)3%|7%T<1?2+NS|Ju8egnzVCzjhbXEr7}jUo*iET;pFcNl?z~h``KI=!iaUH z#_J;^^ny(waJ-hj4T5@IDor^TpksHm-=a8CZxp)v*`}G%4 zLB8|gn1JkFzUx(L@9_Hcu1(nG<>j@#HX$i4f_bY|MZ}%Wod)yx+)pJO!lV2dc3o|F zL6=F)mYR=u{@TOa!5EfsS%!g0Wj6~>fl`FX?J2(GAeX=nd39ItESD$=Fo8Z^c@zx! zFqe!d<9-vHj4FDDS=ooIjH*#Nj~ppf$w~-pj=sDd^Xk`&(18-|;c}(*t2Yd60`167 zR|GQwBElhGnF=RAr*_zf5R~0$hRC2?oi`^Iwqe}JX>^3mlL>@!^sRRA5)QO)lluDi z;cggrcQWC9oej)Fy2LkJvME{VzAHE(!#vHfobJ(XhI`;Bq3yZ*O)PV{;zHiF`!NkY zCWWRRj(^H7-_V_LuoxEyUKV)Z@2sohX$@vL#Duy)s-*82wg#`J{8iaTh6rJ%tCvaK zNYy9~6sF0`JI2HSGHXmufmA5ICM=bvU2fF;N&4&9&Gobmd&O6ilg#ooea=a|!$j5* zW}MmK`;~%2GF%76M2Aq!Ad8-k&PiZc8uw>1NOWhHtDgi{bmRz?IutVYr7V$Q}d-9Bjg!a##14p?=vG z2Kn$NW)ZlHyz1;#Lgn}%W!W%6FesBHolz=YAGJuD)?QxWZ{<6}zT#O}RENrMY$Wi# zH1iGCzhA@M(y;v=Ipb|pBU{37(_8c&p^HVoHS+GT2@M!oBOCep*0_^Jw=qz&>eFiN zyiHfD_nmAC(w1WoZi&MDPiu$tK(NH!+yN<%-qoVpTG4L#alvz_w_^g;FlQa*y%PQ zf;`wM2@dzaF5Eqq-gCBLH7&2ZreMog>^gBmXA)G)}lM9>|ShdNo6KaD*z$vD*{!&6oZ(aC4M+{u^6o}io>J~Fkl(^RSE-} zMJwdzmCHA}y%+Mu{4ostxoh#tE~lJsUSjpZ`OjZl0^|O%;RdK8fFJUYwTbGPOD{W{ zIhQ{I$kOxXvfLEH_0j7)@k_1u5ipw zGOO)4NO3{X{%azY?eD&a=GxP%9cx@U&o3-QTF=+6mOcJ1JD!kXNoDQC;nm1Us&On> z_zH*0d0EA@{PcK*GAo3r_B>KjvANLT=mw0X{+UAsCSIw9pM3@ErA>#OrMgXP2QDqq z3mypSoGN2mOLO_&?X0yym*dQrJazQ>4R(7jcaVco6V?$N8&1*~O`bvu%+*>yiz$~N z&tlVh_qh2V7xZplb>LQmsp_h#7UvcgykOH8jT}QZ*m@j4M?G229nJzl`$PctHJU67 z;HxJl-8iHgo?%}=Tm15Q#K65-0BcKmJs9s&E5Bg7q?08~?vlrW$!PLhPt&kBo4f}r z)2v0xF}-pO6{4Ict5GYJ@G0iYcp02TOirWtg@~KgKK2A;j`x@agW5W_1sChL9ufWG zV3z)(n-C^Q*yDrih-<)EJa{Ut~k}QJgN$46)XNnWM zLudzRg=_;lQ<6wz&UM&dpJm~Pp3RABw%isC8X9Qwn~n6RZ0Oq0pj;JdxYhiRhIigaWk1&7in~x87>+OEf)PlAq*soPpcjR;wZE@8lzg@qt&kiC~@LH5}SnrMM zWyS$l6D>BnUmY%5fgS^duc7a+`-G#B%s@zk&y$S-39m~(i*^|QOrYzn3bR%#C+W5D ztYJ|l8^6`<2$ZzJI@{jZ=(fJuYkRaZ&LmM$1OqR_)ai zg6nZ3v$jrdVa{JQz&8ff58f~<2cT3|jt`(zJYUNgpSjt`)RxvZFaK9%*=&g_-b?6Jz1 zHH1bJD<3M6i&3>opE?I?*N8&I>R2j*YQZj&WMZtBi3e3`lj1=4t&M#GycEMiZDa7C z-Q-IC2`KyfM47&DGZ0;ILWt8vM=+ei)&);~ZkPQc4jD&1uID8VjRzKmY9vLq(9EE? z7~&#(X=&*QJXWb$d@q)bUn!SxNaC@EG{v zVhM)s^T}P3{8)=;K(&`Sky@`GJ*V87T<<^RD9g_Zb?(D#`Y1A(E);Y1iBcSYG#}<$1Pdmm9mc0 zc(s?wnaF^(fG`g?(^={^^SUMzX@}PZKjPQ2R)2uIR=S#HE_Sy^qHvrc^L41eh(ZKB z6~eVJ4LQa@F2>}lsT?i$8h`2Way3%4kvw(DC#L`i`Gq@}xIXi#G4 zp}V`ghmvkax;q9$L6A=AZlt@rB?R=lyr1WO-{bgR{spc%_ul7P>$mb|&8#{D^vhbK z9mmv1!fX8EpB;%B`P?l^3H{Vuv0bWrdfr%HgfkxO>Fw>k+!eVtcihJ6GX4K7X#EZC zRdFoF$(wvIiOnTZwHWe0IbUweelNuttP9e=fE4iY%{_!|2BBeA8W-qwDJpT~q*hRc z2d}bs{C((v0~zI?q};2DMLSdND=j-_ChPC1kG}~wK*iw(E+G%gK%=!CsA8Eni-+}! z=~ucZhts8^;mNdfG*jSXe_(6`Ic(qyW=ib#^`LR#{SSOxQj0KL-I7n z?Cfmc!-s+g*s4c`#rKZH6UxM+Sy%qZ5*H9MW2=UyX6Dv5tVO6U76`G){pHpT*44XH z$`vT80Cx zURMk`{@U{mC>GG)FY}34;i>uiEm#Tcb9cdct6|)o|Jr{>s5|0Sc(KVso&Pgw>oyPf^pid-~mC%-5G zI&}IJN-irhaED4nW{IzJ@iwXMVe#^oIP^OLXpXAzq#3F2ADwy!mR{;Bz2XfqsfjzY zYnAsQlnMBI4$y6SfB1g*_TRkQsQCZxJsi=-=~mPCJ0-^HV2f`+r0VbU_eO!hiqkB3 zlUO&S1L_A9p>dmV`4#`wrUZZzvACElI-ZZ@$mUD-{DReb@V_!D^cq z`p&~#I*43~Xno;`MeG9QS3v4D(7g1_tZA8x6>+iEr1IeXXPzQ^Qwcybx8#3R<2jeT}mzrb% zgyW>I6OzyE^2#H2h{n?VDZ6F*vfN4c_0{1fF}56j8{pWH<=`~5_)MMcSE38;(Jz-L zUDQRN$+ipj;JxC(?7m;@?HVkqSy&C)DzTcZJ$_c-t{Jo0bd-={7QXf(?qiRG zBhYZbXoA;ZD-LKEyOdU*IMGJBF|2v&Mb*yO@eZP4fOdgngwIG<{;`+cq@ND!&s4}z z;`EF9f})n*37AVYPQA*tHr19fk(|$|nhduo@oI9cE~ z@MU5+(uIkQgF;p%A{hI_^UzgMkR*dJN3I^eCxAyVyDCd^JQqPaagwA-L_Mt>& zKKh+QtPx;<*wc0qU`FxTyNA`mig?$=&;Z-^V5$T%VJ2$e8*{(#65n0(tFFZhn)(nI zh3;Cn-4V{cupN95sAuW(Of%z090lod?i=)8y69Ae?&cmNpQHHnw!qwR8+Nj7RLT4j3iPo0pdB*^8|*+{C_;qzGtKVQ4ms`o&S`-SfZ{ zMwdw8CUVwoB=vBSrVuTw>0D!qcf_0BV074^5UC=>X=;))Zy1*l!*t;aEN6bESF4CF z%2@I7*9!T0tcNJIC8a0K9PBr;8x@_Pwz#xz@*H+6&ez)uMH?B091Bpg3=c?YLK1LN z`0)*`mupc^Z=O+U_H7<<$D!bbmdYB09kLS+N4N8$Tx7sEiL ztd?!DmhfaW#pWSg1Uk)9$-Bx1^IHM`cYjr)N?^7gsE&#vFBpPT&P6`DAKv_fa^Wl^ zH~zn4ix?19pNVKOj9I_!rHIBA`+Gk=`cX_oOa0A;s1zz}bRQ-T5_&sU3ql*q6Lad? zC#&_oFr`*6m&ObuilzY(x#zr#76wVD=k2H-Py9}0H*ySpjRpcWy{b!czI`37Xb&&_ z32V}zjPRMkO1!}Z1#5Reix3tSjj1Uq7V&kYm6W~h`SkFksNxGlxS=rC#k_BJ~Av1HTVNV$>{IgUwb zO};nz%im%qfzd)!$u0aMC*i|>B-VT1ZRo-(W54HDMD7hRyu^Xw6=ethWu#D0KIu)B z0p~^&3L4-x|6@q~0#mzj2^w~L?6`a%%klkn2*EL!0#=Ed#M-^;b)O(Tbyd!Hm~SD* z7(=d+sU?NjK)*r9))v}5?fn*p8Y?2P%WHgB_%e3jOFz1g)#Ok=2Zc4wZHk{?;^DJm zDm{AZVW4jeEH*2ZB667Af$1{F6yqXGI7z_HUi#UQ`e|c6ewHsY>S!(24ta&f~$U4if(K^jx*5I!N#sG zaWw)IroQrty8&wWTguo87$e<y**N1B?%_qR+o&A}{V`dv$Pd~3V*a8zCZdpfd97G>e)GU5=0dQS z4o&gemg{y9i z^JtsSknXazH46P=!R@KY+Zt=Ja28?XW1>`_fk;Ibvz}lGMwV8@9C}rL2wWdOG1*U2 zRUNIg^GFmayOKX-Wm2R}Uo&KiZ2UBueQ%w`*!{l0Xgy9HKR^PMMc!)(Bgz{wI5AR06zcfI&#G z46*~`fLLE*liN!Yp}i#C-_weN6EmR&OBjE({~(w-UGpKU z5dEm=Taig98ki-bi=r*oJNFfuTy2bxPQ6+tWEn{x3LjfOt zw+{I@`)x*^!`xAES=m7G-u;G8$-K;drqVI;VyPPEXYT=iSWGoWbVle6VJ}m(W;O)w z0icfNu_2kHYrvWC`iv6%Kl|$l$a53GX(aZ)pWiC~e2-Uz`EUTC=Z=~&F;*yzk<65G zI@-!}&??)5aO$}@V~g@s=#HP%S;}3@L;~MigD_R7dRLvH(zss293>dDG-ApWOqrpe0^(kx|Z-^lhG8fWcM*=&D2`#zRb*?(3B|7K&~~_P9g3E%$MdRQlCTwqQ8_= zvSek)>`c2RCHiQ}62zBjwZs@5)XNKkYSgTs-18 zn>7$a`Y3TOocXFt&?Hi!swVIdcB>O2CAy*pj(^Jn^iKaDt8~jGk^kLI|BJg7n|T9y z%!Zf0Tjz_#KSgs_e(Lr}V<1+cSy#7tF-;-h+7n4~$XG1?Lh3o1I>?ZrZz3y<-|vni z;P-T1CJ%@qJ>EvW=T(?E9a-;Na6e8$?ZrJ{M>%t(D+i}WTxt& zG0kl*NIFq2Rs`dnAimZ{oR}C1|GC=&pyk;(!Ri=PGf>zwPId>?iI)xtJ9@b64a!3t zQg$h|daT*oIqAZ$EnZi7)ZOz#utpClq0^(^dcbY9HUO9t2@Joj-y%AEkE{DP_tFY8 zqZNv8P#ua4S`EDwoM^-94}idhh$5|35V#+H3=V11@VDu<( z>3Vt6P)L{+90YxRYdYN^bCWk6VQIW%;{HYu{j64PSA{fD^bCx=aqLSeMV+LznZfoK zEIW-IX>*@Q@AOL0jn<)4Mj-V{lSko(HZLPwX&P8{2e%qlGd60e+HWTNAQPS>`R<5f z=+%-??-&Z22C$xHc97kUEyYLd&aRj1czD};wq3BPb`IiaCfaPlq7}SE?H2wL^=A!! zI+Ff5+`oy1frDl6P2zAZ$aj}-eRV~ZW{D(*S66F2;_yE3d6aa3o|BU*R2 za*2S1&j?&2l{*jHT!gq|u$5Te9w5eFveeD|N;&@c^*wkOXGCJO%0lKNjAkJe<7X&X zfB3sXq~b1iQnh`<<7!#PW`+oW&LVwri{cyO?DQ|;_Rqz8{9l{Hdg-}+A%{3laIPZh zBRvD|d3c@67KHlCKFB3Ab@jt^sUmk*5EhgsMi8UO_yc8x#nFrhhE*<*wwA8x*gY9D6*(@Yel$vhNGT zQPEvVz3o$IwBog>>cguWMk%tKvG|E!%|PZIu}^KCHv3ht&I6zZmcn5|Vy#gApA?8m zv%j{&*5rSPKjM&?dh%N(FjrU$x1#&G zzA^)AO|QmN8gWQ|VCtnLb58bq9>+Je`QwQ3P5PJhnaH50;oRNtpLLF{6?e&}Jw&lC2$NRchZak5|8u^E&0u+>|%f!X9!H$_23hH)lLj z_@7ojzoz_>tsPm2Vln^d$$O}E+V^K(CHrf#)h`pDXRZ#kX!3r^hakV@IkR@$bCbdT z2K%_ZMA4;DwL1AkXB1kfZQ7e0o=U#YAI+zS9p$Vt!}Q7sm7GaC zxd2YiFq~!f>p#X$95?L%;TNz$0}$B%`aT>pd9xT^sJu|AmUYr3easW~sloBt_mNb7 zU-*pE@9w;jPmzm94CM`)Vm4cxv(zgzauzKSz-zBelOf<{DQ^F!%nM7za$n%r&5~1= z*==~HpBtc}8;VtWVQ(y+50_3BX%PU(k#IvH46Tiwiu`(-B}w6jF1W~q#Z++CXcWFslGOQEl3;pU?Py@Ea-TZwT`pW%R1 zSG~6|saL;YR7OZTqY1(~CZ?s@N zQfA_qs~yHhW$;TxH+49m}-eK#xu*;s$&Ean{5QYKJd`yuO@aj%9_;kGILZeZ*N; z)4!3Om1u1x)iU`d=tiY<9joh|eZD@?&NQ@W%~;J+4{D%u=srr~GqbVeWzstBZr|)~ zRA(L^*}7C#bD|X$zqcq~mGvXN{1x=zhk+)vMEeO2c|FH?cAY~*b z>5;0Uf-3K&&6B_s`?*r=L%AZ`Us^ZYDgKj*$)ugR*Z*5R!H538dACN!(#fs&$h}0- z;)g+!9QwT|(QghtB&$Cbd&O6F7VAahGVz~>OS^>ms55M;J1AWJK*nAV!4~D%#2nTv zkU|Via}g@d$(@X8;T5$A=7B5A->UUp+FY=9a|Cy-C$Q8%jVg$F+DQ7urM>*Ow8r5dbCXT(4H0`lesQZJNQ-NP|$C$x)yOzL7!m?0Haufw1Rgcrmu3 zkbNe9Oe5LHO7YiltPft+-$=2!#ivWMRjk<+pJVwyt4$rmwkLC@(~1{+UO80nga6G8 zXH2rcu*H_r`475m8YYkBrrb4UZK!l%#Wq`Q1ZvUJmnYE7xS;R6B3HJ1d~fCH`KSD| zTKJ0Sq_CqXeTRLYz z8sRY7Vf_J8=G71XK7l!(Ru;PsM~5~q**34NW}&zh7U}8l7>N%b8aWamZ#Th_djZb& zOEyb(Tf7RJiiWRa8bbPKZ?>=N*>OFy_!}_zzS^jb3vT&jyIEXPE}E&kP+G`;7q~XYV-=TLPjD zNy{W8adVRc_i>G0jw0mgdNIj5b>5D0n4&q#F|95sk)m}dy-N1rSW%6rl7qT*-Xdc7 zSb3kn+o8+&a!&Z|m`(B)M3}z<2R>HTpEP~+iOT_%4qDzF%P92C_!3K4s063uO*?1F z>oe$T6r9U*otyD2rdF3lekmTme5snNM6O;#8(ct~r}U+_g-FBO!B}dsdehw* z#dIzo%ywb|z1vG8R5Z$L>t_eTxCI&JcoaEaQ#)W>$@gS+^2Da|Hu6dOt$~L+Ign; zC%3{@l@J`Ah7zQ@}DazcJakFf`TUf!aH8AnVqp;Gk zR+mUxZ4wpnL)guM02L8L$Xk?=YSK)83((s)3)2_#Ef~_n?|%x#&w$VU#D4D=?FuTp z$aQ)?0d@fJ&QT_m$@0(o!woUuMrnuJOy@y{e|jFs?3y;Mem__Od`{^$8zVE$AOo6M zGZ;H6jLXVfTAV|R zx7^*k#BKOc$Emv~2VpS>K{FI&C|>D7Hq;m{{>6UfeBxPu2y!^9nVB|BQmK7(AVH*V zBuS4r)x0boL$hBHATovcNtSfg zZ$~pCN*vAX8m}u)(Y=$%$xj&YY*eMZ#r>V2O@lstPE)Pa@3m}-a;+ip_IbKF2_XbQ z@zebjp_Ew4(Z^|mMbSg1Pwt6pXs(H+cYD@JZvEw$DqOf}C94${@Gbod@9Zeyp!!V} z>jcpgmPFA=*+7C=ao^M#o64ntsg4HOMtwriJ0FJ{L&L0N>UzZcOZpz5fv9y2i;SnIIv!@2aQ z{;4$py?Fd8*v=rnWxJuPKEzZ*w2IkKD3(yjTjacf&F0?skc}iBUG;CurL=SDHsisX zH0@uJ{p2;f@rS`GxkaMoz-v*FhfSdF^CG+;->Nu}s2(ZbIalqjlMb=YWqt-5@Gr*P)X{Kzh

)mI71-qM5=i}!$$j-i;>8#!MIPC|M--! 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 b&*t*G1XFg6a`qwo9O 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 Date: Sun, 28 Jul 2024 15:09:36 +0200 Subject: [PATCH 017/541] Update de.rs (#8870) --- src/lang/de.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lang/de.rs b/src/lang/de.rs index 3a2e78a7a0ab..3363d7e59665 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -630,7 +630,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("cancel-2fa-confirm-tip", "Sind Sie sicher, dass Sie 2FA abbrechen möchten?"), ("cancel-bot-confirm-tip", "Sind Sie sicher, dass Sie Telegram-Bot abbrechen möchten?"), ("About RustDesk", "Über RustDesk"), - ("Send clipboard keystrokes", ""), - ("network_error_tip", ""), + ("Send clipboard keystrokes", "Tastenanschläge aus der Zwischenablage senden"), + ("network_error_tip", "Bitte überprüfen Sie Ihre Netzwerkverbindung und versuchen Sie es dann erneut."), ].iter().cloned().collect(); } From fd9b5f3c57cb8b9928c99532892af32bc78ee8c2 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Sun, 28 Jul 2024 22:55:00 +0800 Subject: [PATCH 018/541] fix https://github.com/rustdesk/rustdesk-server-pro/issues/334 --- src/client.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/client.rs b/src/client.rs index 6fc8ce1a6af8..d6d8f8546162 100644 --- a/src/client.rs +++ b/src/client.rs @@ -2110,6 +2110,13 @@ impl LoginConfigHandler { (my_id, self.id.clone()) }; let mut display_name = get_buildin_option(config::keys::OPTION_DISPLAY_NAME); + if display_name.is_empty() { + display_name = serde_json::from_str::>( + &LocalConfig::get_option("user_info"), + ) + .map(|mut x| x.remove("name").unwrap_or_default()) + .unwrap_or_default(); + } if display_name.is_empty() { display_name = crate::username(); } From 0a1d3c4afb1c2d8eacfdd0d41f1df0bcbc150094 Mon Sep 17 00:00:00 2001 From: XLion Date: Mon, 29 Jul 2024 15:34:26 +0800 Subject: [PATCH 019/541] Update tw.rs and cn.rs (#8877) * Update tw.rs * Update cn.rs --- src/lang/cn.rs | 12 ++++++------ src/lang/tw.rs | 6 +++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 2b33a853470a..48a0875fca73 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -624,13 +624,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Volume up", "提升音量"), ("Volume down", "降低音量"), ("Power", "电源"), - ("Telegram bot", ""), - ("enable-bot-tip", ""), - ("enable-bot-desc", ""), + ("Telegram bot", "Telegram 机器人"), + ("enable-bot-tip", "如果您启用此功能,您可以从您的机器人接收双重认证码,亦可作为连线通知之用。"), + ("enable-bot-desc", "1. 开启与 @BotFather 的对话。\n2. 发送命令 \"/newbot\"。 您将在完成此步骤后收到权杖 (Token)。\n3. 开始与您刚创建的机器人的对话。发送一则以正斜杠 (\"/\") 开头的消息来启用它,例如 \"/hello\"。"), ("cancel-2fa-confirm-tip", "确定要取消双重认证吗?"), ("cancel-bot-confirm-tip", "确定要取消 Telegram 机器人吗?"), - ("About RustDesk", ""), - ("Send clipboard keystrokes", ""), - ("network_error_tip", "请检查网络连接, 然后点击再试"), + ("About RustDesk", "关于 RustDesk"), + ("Send clipboard keystrokes", "发送剪贴板按键"), + ("network_error_tip", "请检查网络连接,然后点击再试"), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 9723bfde59e8..8fa1c8c14e71 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -629,8 +629,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("enable-bot-desc", "1. 開啟與 @BotFather 的對話。\n2. 傳送指令 \"/newbot\"。 您將會在完成此步驟後收到權杖 (Token)。\n3. 開始與您剛創立的機器人的對話。 傳送一則以正斜槓 (\"/\") 開頭的訊息來啟用它,例如 \"/hello\"。"), ("cancel-2fa-confirm-tip", "確定要取消二步驟驗證嗎?"), ("cancel-bot-confirm-tip", "確定要取消 Telegram 機器人嗎?"), - ("About RustDesk", ""), - ("Send clipboard keystrokes", ""), - ("network_error_tip", ""), + ("About RustDesk", "關於 RustDesk"), + ("Send clipboard keystrokes", "發送剪貼簿按鍵"), + ("network_error_tip", "請檢查網路連結,然後點擊重試"), ].iter().cloned().collect(); } From 0e98a517752ccac7d51c5280f5604ba6cc0fd735 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Mon, 29 Jul 2024 15:40:02 +0800 Subject: [PATCH 020/541] fix: clipboard, set formats and enable option (#8873) Signed-off-by: fufesou --- src/client/io_loop.rs | 4 ++-- src/clipboard.rs | 37 +++++++++++++++++++++++-------------- 2 files changed, 25 insertions(+), 16 deletions(-) diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index a182a5fe0224..ea2afff1dd7e 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -1367,17 +1367,17 @@ impl Remote { // https://github.com/rustdesk/rustdesk/issues/3703#issuecomment-1474734754 match p.permission.enum_value() { Ok(Permission::Keyboard) => { + *self.handler.server_keyboard_enabled.write().unwrap() = p.enabled; #[cfg(feature = "flutter")] #[cfg(not(any(target_os = "android", target_os = "ios")))] crate::flutter::update_text_clipboard_required(); - *self.handler.server_keyboard_enabled.write().unwrap() = p.enabled; self.handler.set_permission("keyboard", p.enabled); } Ok(Permission::Clipboard) => { + *self.handler.server_clipboard_enabled.write().unwrap() = p.enabled; #[cfg(feature = "flutter")] #[cfg(not(any(target_os = "android", target_os = "ios")))] crate::flutter::update_text_clipboard_required(); - *self.handler.server_clipboard_enabled.write().unwrap() = p.enabled; self.handler.set_permission("clipboard", p.enabled); } Ok(Permission::Audio) => { diff --git a/src/clipboard.rs b/src/clipboard.rs index fe30189caaaa..44a3c09d3859 100644 --- a/src/clipboard.rs +++ b/src/clipboard.rs @@ -3,9 +3,7 @@ use clipboard_master::{ClipboardHandler, Master, Shutdown}; use hbb_common::{log, message_proto::*, ResultType}; use std::{ sync::{mpsc::Sender, Arc, Mutex}, - thread, thread::JoinHandle, - time::Duration, }; pub const CLIPBOARD_NAME: &'static str = "clipboard"; @@ -18,6 +16,10 @@ lazy_static::lazy_static! { static ref ARBOARD_MTX: Arc> = Arc::new(Mutex::new(())); // cache the clipboard msg static ref LAST_MULTI_CLIPBOARDS: Arc> = Arc::new(Mutex::new(MultiClipboards::new())); + // Clipboard on Linux is "server--clients" mode. + // The clipboard content is owned by the server and passed to the clients when requested. + // Plain text is the only exception, it does not require the server to be present. + static ref CLIPBOARD_UPDATE_CTX: Arc>> = Arc::new(Mutex::new(None)); } const SUPPORTED_FORMATS: &[ClipboardFormat] = &[ @@ -162,20 +164,27 @@ fn update_clipboard_(multi_clipboards: Vec, side: ClipboardSide) { if to_update_data.is_empty() { return; } - match ClipboardContext::new() { - Ok(mut ctx) => { - to_update_data.push(ClipboardData::Special(( - RUSTDESK_CLIPBOARD_OWNER_FORMAT.to_owned(), - side.get_owner_data(), - ))); - if let Err(e) = ctx.set(&to_update_data) { - log::debug!("Failed to set clipboard: {}", e); - } else { - log::debug!("{} updated on {}", CLIPBOARD_NAME, side); + let mut ctx = CLIPBOARD_UPDATE_CTX.lock().unwrap(); + if ctx.is_none() { + match ClipboardContext::new() { + Ok(x) => { + *ctx = Some(x); + } + Err(e) => { + log::error!("Failed to create clipboard context: {}", e); + return; } } - Err(err) => { - log::error!("Failed to create clipboard context: {}", err); + } + if let Some(ctx) = ctx.as_mut() { + to_update_data.push(ClipboardData::Special(( + RUSTDESK_CLIPBOARD_OWNER_FORMAT.to_owned(), + side.get_owner_data(), + ))); + if let Err(e) = ctx.set(&to_update_data) { + log::debug!("Failed to set clipboard: {}", e); + } else { + log::debug!("{} updated on {}", CLIPBOARD_NAME, side); } } } From e03344d85bbd61b8098b0966419e3453b662a800 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Mon, 29 Jul 2024 21:19:29 +0800 Subject: [PATCH 021/541] fix: clipboard, read as much as possible (#8881) Signed-off-by: fufesou --- Cargo.lock | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d994c404d2a9..7a51371b5a75 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -224,7 +224,7 @@ checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" [[package]] name = "arboard" version = "3.4.0" -source = "git+https://github.com/rustdesk-org/arboard#75166f255bf2fd6c662269029f5130b11d024f46" +source = "git+https://github.com/rustdesk-org/arboard#a04bdb1b368a99691822c33bf0f7ed497d6a7a35" dependencies = [ "clipboard-win", "core-graphics 0.23.2", @@ -234,6 +234,8 @@ dependencies = [ "objc2-app-kit", "objc2-foundation", "parking_lot", + "serde 1.0.203", + "serde_derive", "windows-sys 0.48.0", "wl-clipboard-rs", "x11rb 0.13.1", @@ -1675,7 +1677,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" dependencies = [ - "libloading 0.7.4", + "libloading 0.8.4", ] [[package]] From 764fbe2c9dfed713fff14605ecb7922a33d9f4e8 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Tue, 30 Jul 2024 00:26:38 +0800 Subject: [PATCH 022/541] addressing https://github.com/rustdesk/rustdesk/issues/8883 --- flutter/lib/models/model.dart | 40 +++++++++++++++++------------------ flutter/lib/utils/image.dart | 5 ----- 2 files changed, 20 insertions(+), 25 deletions(-) diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 2ad9fdb9b785..cf93fde42e4b 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -1175,26 +1175,26 @@ class ImageModel with ChangeNotifier { clearImage() => _image = null; - onRgba(int display, Uint8List rgba) { + onRgba(int display, Uint8List rgba) async { + try { + await decodeAndUpdate(display, rgba); + } catch (e) { + debugPrint('onRgba error: $e'); + } + platformFFI.nextRgba(sessionId, display); + } + + decodeAndUpdate(int display, Uint8List rgba) async { final pid = parent.target?.id; final rect = parent.target?.ffiModel.pi.getDisplayRect(display); - img.decodeImageFromPixels( - rgba, - rect?.width.toInt() ?? 0, - rect?.height.toInt() ?? 0, - isWeb ? ui.PixelFormat.rgba8888 : ui.PixelFormat.bgra8888, - onPixelsCopied: () { - // Unlock the rgba memory from rust codes. - platformFFI.nextRgba(sessionId, display); - }).then((image) { - if (parent.target?.id != pid) return; - try { - // my throw exception, because the listener maybe already dispose - update(image); - } catch (e) { - debugPrint('update image: $e'); - } - }); + final image = await img.decodeImageFromPixels( + rgba, + rect?.width.toInt() ?? 0, + rect?.height.toInt() ?? 0, + isWeb ? ui.PixelFormat.rgba8888 : ui.PixelFormat.bgra8888, + ); + if (parent.target?.id != pid) return; + await update(image); } update(ui.Image? image) async { @@ -2558,7 +2558,7 @@ class FFI { final rgba = platformFFI.getRgba(sessionId, display, sz); if (rgba != null) { onEvent2UIRgba(); - imageModel.onRgba(display, rgba); + await imageModel.onRgba(display, rgba); } else { platformFFI.nextRgba(sessionId, display); } @@ -2626,7 +2626,7 @@ class FFI { canvasModel.scale, ffiModel.pi.currentDisplay); } - imageModel.update(null); + await imageModel.update(null); cursorModel.clear(); ffiModel.clear(); canvasModel.clear(); diff --git a/flutter/lib/utils/image.dart b/flutter/lib/utils/image.dart index 6280555ba4f4..d5042e61ad2a 100644 --- a/flutter/lib/utils/image.dart +++ b/flutter/lib/utils/image.dart @@ -13,14 +13,12 @@ Future decodeImageFromPixels( int? rowBytes, int? targetWidth, int? targetHeight, - VoidCallback? onPixelsCopied, // must ensure onPixelsCopied is called no matter this function succeeds bool allowUpscaling = true, }) async { if (targetWidth != null) { assert(allowUpscaling || targetWidth <= width); if (!(allowUpscaling || targetWidth <= width)) { print("not allow upscaling but targetWidth > width"); - onPixelsCopied?.call(); return null; } } @@ -28,7 +26,6 @@ Future decodeImageFromPixels( assert(allowUpscaling || targetHeight <= height); if (!(allowUpscaling || targetHeight <= height)) { print("not allow upscaling but targetHeight > height"); - onPixelsCopied?.call(); return null; } } @@ -36,9 +33,7 @@ Future decodeImageFromPixels( final ui.ImmutableBuffer buffer; try { buffer = await ui.ImmutableBuffer.fromUint8List(pixels); - onPixelsCopied?.call(); } catch (e) { - onPixelsCopied?.call(); return null; } From 97772f9ac512a88bf4a6ed19a10911c6294cb0b8 Mon Sep 17 00:00:00 2001 From: "Generalworks Inc." Date: Tue, 30 Jul 2024 01:27:28 +0900 Subject: [PATCH 023/541] Fix typo on ja.rs (#8886) --- src/lang/ja.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lang/ja.rs b/src/lang/ja.rs index c2537ebc9d36..7bc06d8ccca6 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -430,7 +430,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("length>=8", "8文字以上"), ("Weak", "脆弱"), ("Medium", "普通"), - ("Strong", "協力"), + ("Strong", "強力"), ("Switch Sides", "接続方向の切り替え"), ("Please confirm if you want to share your desktop?", "デスクトップの共有を許可しますか?"), ("Display", "ディスプレイ"), From 15404ecab4b5cca0d02f5d8e357123e5bb3d26d6 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Tue, 30 Jul 2024 11:35:39 +0800 Subject: [PATCH 024/541] fix: clipboard, windows, controlled side, formats (#8885) * fix: clipboard, windows, controlled side, formats Signed-off-by: fufesou * Clipboard, reuse ipc conn and send_raw() Signed-off-by: fufesou * Clipboard, merge content buffer Signed-off-by: fufesou * refact: clipboard service, ipc stream Signed-off-by: fufesou --------- Signed-off-by: fufesou --- src/clipboard.rs | 29 +++++++- src/ipc.rs | 36 +++++++--- src/server/clipboard_service.rs | 116 +++++++++++++++++++++++++++++++- src/ui_cm_interface.rs | 62 +++++++++++++---- 4 files changed, 216 insertions(+), 27 deletions(-) diff --git a/src/clipboard.rs b/src/clipboard.rs index 44a3c09d3859..0510eca6a2d4 100644 --- a/src/clipboard.rs +++ b/src/clipboard.rs @@ -16,10 +16,11 @@ lazy_static::lazy_static! { static ref ARBOARD_MTX: Arc> = Arc::new(Mutex::new(())); // cache the clipboard msg static ref LAST_MULTI_CLIPBOARDS: Arc> = Arc::new(Mutex::new(MultiClipboards::new())); - // Clipboard on Linux is "server--clients" mode. + // For updating in server and getting content in cm. + // Clipboard on Linux is "server--clients" mode. // The clipboard content is owned by the server and passed to the clients when requested. // Plain text is the only exception, it does not require the server to be present. - static ref CLIPBOARD_UPDATE_CTX: Arc>> = Arc::new(Mutex::new(None)); + static ref CLIPBOARD_CTX: Arc>> = Arc::new(Mutex::new(None)); } const SUPPORTED_FORMATS: &[ClipboardFormat] = &[ @@ -159,12 +160,34 @@ pub fn check_clipboard( None } +#[cfg(target_os = "windows")] +pub fn check_clipboard_cm() -> ResultType { + let mut ctx = CLIPBOARD_CTX.lock().unwrap(); + if ctx.is_none() { + match ClipboardContext::new() { + Ok(x) => { + *ctx = Some(x); + } + Err(e) => { + hbb_common::bail!("Failed to create clipboard context: {}", e); + } + } + } + if let Some(ctx) = ctx.as_mut() { + let content = ctx.get(ClipboardSide::Host, false)?; + let clipboards = proto::create_multi_clipboards(content); + Ok(clipboards) + } else { + hbb_common::bail!("Failed to create clipboard context"); + } +} + fn update_clipboard_(multi_clipboards: Vec, side: ClipboardSide) { let mut to_update_data = proto::from_multi_clipbards(multi_clipboards); if to_update_data.is_empty() { return; } - let mut ctx = CLIPBOARD_UPDATE_CTX.lock().unwrap(); + let mut ctx = CLIPBOARD_CTX.lock().unwrap(); if ctx.is_none() { match ClipboardContext::new() { Ok(x) => { diff --git a/src/ipc.rs b/src/ipc.rs index 489db38e7098..fba0000fb6a2 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -1,10 +1,3 @@ -use std::{ - collections::HashMap, - sync::atomic::{AtomicBool, Ordering}, -}; -#[cfg(not(windows))] -use std::{fs::File, io::prelude::*}; - use crate::{ privacy_mode::PrivacyModeState, ui_interface::{get_local_option, set_local_option}, @@ -14,6 +7,12 @@ use parity_tokio_ipc::{ Connection as Conn, ConnectionClient as ConnClient, Endpoint, Incoming, SecurityAttributes, }; use serde_derive::{Deserialize, Serialize}; +use std::{ + collections::HashMap, + sync::atomic::{AtomicBool, Ordering}, +}; +#[cfg(not(windows))] +use std::{fs::File, io::prelude::*}; #[cfg(all(feature = "flutter", feature = "plugin_framework"))] #[cfg(not(any(target_os = "android", target_os = "ios")))] @@ -26,8 +25,11 @@ use hbb_common::{ config::{self, Config, Config2}, futures::StreamExt as _, futures_util::sink::SinkExt, - log, password_security as password, timeout, tokio, - tokio::io::{AsyncRead, AsyncWrite}, + log, password_security as password, timeout, + tokio::{ + self, + io::{AsyncRead, AsyncWrite}, + }, tokio_util::codec::Framed, ResultType, }; @@ -100,6 +102,20 @@ pub enum FS { }, } +#[cfg(target_os = "windows")] +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(tag = "t")] +pub struct ClipboardNonFile { + pub compress: bool, + pub content: bytes::Bytes, + pub content_len: usize, + pub next_raw: bool, + pub width: i32, + pub height: i32, + // message.proto: ClipboardFormat + pub format: i32, +} + #[cfg(not(any(target_os = "android", target_os = "ios")))] #[derive(Debug, Serialize, Deserialize, Clone)] #[serde(tag = "t", content = "c")] @@ -207,6 +223,8 @@ pub enum Data { #[cfg(not(any(target_os = "android", target_os = "ios")))] ClipboardFile(ClipboardFile), ClipboardFileEnabled(bool), + #[cfg(target_os = "windows")] + ClipboardNonFile(Option<(String, Vec)>), PrivacyModeState((i32, PrivacyModeState, String)), TestRendezvousServer, #[cfg(not(any(target_os = "android", target_os = "ios")))] diff --git a/src/server/clipboard_service.rs b/src/server/clipboard_service.rs index c0d081eefffa..d07fc74b43b4 100644 --- a/src/server/clipboard_service.rs +++ b/src/server/clipboard_service.rs @@ -3,6 +3,8 @@ pub use crate::clipboard::{ check_clipboard, ClipboardContext, ClipboardSide, CLIPBOARD_INTERVAL as INTERVAL, CLIPBOARD_NAME as NAME, }; +#[cfg(windows)] +use crate::ipc::{self, ClipboardFile, ClipboardNonFile, Data}; use clipboard_master::{CallbackResult, ClipboardHandler}; use std::{ io, @@ -14,6 +16,8 @@ struct Handler { sp: EmptyExtraFieldService, ctx: Option, tx_cb_result: Sender, + #[cfg(target_os = "windows")] + stream: Option>, } pub fn new() -> GenericService { @@ -28,6 +32,8 @@ fn run(sp: EmptyExtraFieldService) -> ResultType<()> { sp: sp.clone(), ctx: Some(ClipboardContext::new()?), tx_cb_result, + #[cfg(target_os = "windows")] + stream: None, }; let (tx_start_res, rx_start_res) = channel(); @@ -64,8 +70,10 @@ fn run(sp: EmptyExtraFieldService) -> ResultType<()> { impl ClipboardHandler for Handler { fn on_clipboard_change(&mut self) -> CallbackResult { self.sp.snapshot(|_sps| Ok(())).ok(); - if let Some(msg) = check_clipboard(&mut self.ctx, ClipboardSide::Host, false) { - self.sp.send(msg); + if self.sp.ok() { + if let Some(msg) = self.get_clipboard_msg() { + self.sp.send(msg); + } } CallbackResult::Next } @@ -77,3 +85,107 @@ impl ClipboardHandler for Handler { CallbackResult::Next } } + +impl Handler { + fn get_clipboard_msg(&mut self) -> Option { + #[cfg(target_os = "windows")] + if crate::common::is_server() && crate::platform::is_root() { + match self.read_clipboard_from_cm_ipc() { + Err(e) => { + log::error!("Failed to read clipboard from cm: {}", e); + } + Ok(data) => { + let mut msg = Message::new(); + let multi_clipboards = MultiClipboards { + clipboards: data + .into_iter() + .map(|c| Clipboard { + compress: c.compress, + content: c.content, + width: c.width, + height: c.height, + format: ClipboardFormat::from_i32(c.format) + .unwrap_or(ClipboardFormat::Text) + .into(), + ..Default::default() + }) + .collect(), + ..Default::default() + }; + msg.set_multi_clipboards(multi_clipboards); + return Some(msg); + } + } + } + check_clipboard(&mut self.ctx, ClipboardSide::Host, false) + } + + // It's ok to do async operation in the clipboard service because: + // 1. the clipboard is not used frequently. + // 2. the clipboard handle is sync and will not block the main thread. + #[cfg(windows)] + #[tokio::main(flavor = "current_thread")] + async fn read_clipboard_from_cm_ipc(&mut self) -> ResultType> { + let mut is_sent = false; + if let Some(stream) = &mut self.stream { + // If previous stream is still alive, reuse it. + // If the previous stream is dead, `is_sent` will trigger reconnect. + is_sent = stream.send(&Data::ClipboardNonFile(None)).await.is_ok(); + } + if !is_sent { + let mut stream = crate::ipc::connect(100, "_cm").await?; + stream.send(&Data::ClipboardNonFile(None)).await?; + self.stream = Some(stream); + } + + if let Some(stream) = &mut self.stream { + loop { + match stream.next_timeout(800).await? { + Some(Data::ClipboardNonFile(Some((err, mut contents)))) => { + if !err.is_empty() { + bail!("{}", err); + } else { + if contents.iter().any(|c| c.next_raw) { + match timeout(1000, stream.next_raw()).await { + Ok(Ok(mut data)) => { + for c in &mut contents { + if c.next_raw { + if c.content_len <= data.len() { + c.content = + data.split_off(c.content_len).into(); + } else { + // Reconnect the next time to avoid the next raw data mismatch. + self.stream = None; + bail!("failed to get raw clipboard data: invalid size"); + } + } + } + } + Ok(Err(e)) => { + // reset by peer + self.stream = None; + bail!("failed to get raw clipboard data: {}", e); + } + Err(e) => { + // Reconnect to avoid the next raw data remaining in the buffer. + self.stream = None; + log::debug!("failed to get raw clipboard data: {}", e); + } + } + } + return Ok(contents); + } + } + Some(Data::ClipboardFile(ClipboardFile::MonitorReady)) => { + // ClipboardFile::MonitorReady is the first message sent by cm. + } + _ => { + bail!("failed to get clipboard data from cm"); + } + } + } + } + // unreachable! + bail!("failed to get clipboard data from cm"); + } +} diff --git a/src/ui_cm_interface.rs b/src/ui_cm_interface.rs index 7dae13ebc98c..8374e65a167b 100644 --- a/src/ui_cm_interface.rs +++ b/src/ui_cm_interface.rs @@ -1,16 +1,5 @@ -#[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))] -use std::iter::FromIterator; -#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] -use std::sync::Arc; -use std::{ - collections::HashMap, - ops::{Deref, DerefMut}, - sync::{ - atomic::{AtomicI64, Ordering}, - RwLock, - }, -}; - +#[cfg(target_os = "windows")] +use crate::ipc::ClipboardNonFile; #[cfg(not(any(target_os = "android", target_os = "ios")))] use crate::ipc::Connection; #[cfg(not(any(target_os = "ios")))] @@ -36,6 +25,18 @@ use hbb_common::{ #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] use hbb_common::{tokio::sync::Mutex as TokioMutex, ResultType}; use serde_derive::Serialize; +#[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))] +use std::iter::FromIterator; +#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] +use std::sync::Arc; +use std::{ + collections::HashMap, + ops::{Deref, DerefMut}, + sync::{ + atomic::{AtomicI64, Ordering}, + RwLock, + }, +}; #[derive(Serialize, Clone)] pub struct Client { @@ -486,6 +487,41 @@ impl IpcTaskRunner { Data::CloseVoiceCall(reason) => { self.cm.voice_call_closed(self.conn_id, reason.as_str()); } + #[cfg(target_os = "windows")] + Data::ClipboardNonFile(_) => { + match crate::clipboard::check_clipboard_cm() { + Ok(multi_clipoards) => { + let mut raw_contents = bytes::BytesMut::new(); + let mut main_data = vec![]; + for c in multi_clipoards.clipboards.into_iter() { + let (content, content_len, next_raw) = { + // TODO: find out a better threshold + let content_len = c.content.len(); + if content_len > 1024 * 3 { + (c.content, content_len, false) + } else { + raw_contents.extend(c.content); + (bytes::Bytes::new(), content_len, true) + } + }; + main_data.push(ClipboardNonFile { + compress: c.compress, + content, + content_len, + next_raw, + width: c.width, + height: c.height, + format: c.format.value(), + }); + } + allow_err!(self.stream.send(&Data::ClipboardNonFile(Some(("".to_owned(), main_data)))).await); + allow_err!(self.stream.send_raw(raw_contents.into()).await); + } + Err(e) => { + allow_err!(self.stream.send(&Data::ClipboardNonFile(Some((format!("{}", e), vec![])))).await); + } + } + } _ => { } From 8ced4ddaa27fa2a71166fe83323b5627f06890f5 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Tue, 30 Jul 2024 13:06:37 +0800 Subject: [PATCH 025/541] simple refact (#8888) Signed-off-by: fufesou --- src/ui_cm_interface.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/ui_cm_interface.rs b/src/ui_cm_interface.rs index 8374e65a167b..451de6b239af 100644 --- a/src/ui_cm_interface.rs +++ b/src/ui_cm_interface.rs @@ -494,14 +494,14 @@ impl IpcTaskRunner { let mut raw_contents = bytes::BytesMut::new(); let mut main_data = vec![]; for c in multi_clipoards.clipboards.into_iter() { - let (content, content_len, next_raw) = { + let content_len = c.content.len(); + let (content, next_raw) = { // TODO: find out a better threshold - let content_len = c.content.len(); if content_len > 1024 * 3 { - (c.content, content_len, false) + (c.content, false) } else { raw_contents.extend(c.content); - (bytes::Bytes::new(), content_len, true) + (bytes::Bytes::new(), true) } }; main_data.push(ClipboardNonFile { From cba8aaa4108f7826810b07a43163c41bd3743b5a Mon Sep 17 00:00:00 2001 From: rustdesk Date: Tue, 30 Jul 2024 14:41:36 +0800 Subject: [PATCH 026/541] tooltip for https://github.com/rustdesk/rustdesk/issues/8600, and change dialog error to richtext with link support --- flutter/lib/common.dart | 46 ++++++++++++++++++++++++++++-- libs/scrap/src/wayland/pipewire.rs | 2 +- 2 files changed, 45 insertions(+), 3 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 054b4df310f1..2f526fb483e6 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -1057,6 +1057,49 @@ class CustomAlertDialog extends StatelessWidget { } } +Widget createDialogContent(String text) { + final RegExp linkRegExp = RegExp(r'(https?://[^\s]+)'); + final List spans = []; + int start = 0; + bool hasLink = false; + + linkRegExp.allMatches(text).forEach((match) { + hasLink = true; + if (match.start > start) { + spans.add(TextSpan(text: text.substring(start, match.start))); + } + spans.add(TextSpan( + text: match.group(0) ?? '', + style: TextStyle( + color: Colors.blue, + decoration: TextDecoration.underline, + ), + recognizer: TapGestureRecognizer() + ..onTap = () { + String linkText = match.group(0) ?? ''; + linkText = linkText.replaceAll(RegExp(r'[.,;!?]+$'), ''); + launchUrl(Uri.parse(linkText)); + }, + )); + start = match.end; + }); + + if (start < text.length) { + spans.add(TextSpan(text: text.substring(start))); + } + + if (!hasLink) { + return SelectableText(text, style: const TextStyle(fontSize: 15)); + } + + return SelectableText.rich( + TextSpan( + style: TextStyle(color: Colors.black, fontSize: 15), + children: spans, + ), + ); +} + void msgBox(SessionID sessionId, String type, String title, String text, String link, OverlayDialogManager dialogManager, {bool? hasCancel, ReconnectHandle? reconnect, int? reconnectTimeout}) { @@ -1214,8 +1257,7 @@ Widget msgboxContent(String type, String title, String text) { translate(title), style: TextStyle(fontSize: 21), ).marginOnly(bottom: 10), - SelectableText(translateText(text), - style: const TextStyle(fontSize: 15)), + createDialogContent(translateText(text)), ], ), ), diff --git a/libs/scrap/src/wayland/pipewire.rs b/libs/scrap/src/wayland/pipewire.rs index c7f2e62ac04a..640f37d0b834 100644 --- a/libs/scrap/src/wayland/pipewire.rs +++ b/libs/scrap/src/wayland/pipewire.rs @@ -593,7 +593,7 @@ pub fn request_remote_desktop() -> Result< } } Err(Box::new(DBusError( - "Failed to obtain screen capture.".into(), +"Failed to obtain screen capture. You may need to upgrade the PipeWire library for better compatibility. Please check https://github.com/rustdesk/rustdesk/issues/8600#issuecomment-2254720954 for more details.".into() ))) } From e1e4bf599b0988bac0d26f8584eec72a80514111 Mon Sep 17 00:00:00 2001 From: 21pages Date: Tue, 30 Jul 2024 14:52:06 +0800 Subject: [PATCH 027/541] add libatomic1 to linux armv7 sciter depends (#8890) Signed-off-by: 21pages --- build.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/build.py b/build.py index 97ec9dfe9f9e..389ee33b6cd6 100755 --- a/build.py +++ b/build.py @@ -31,6 +31,11 @@ def get_deb_arch() -> str: return "amd64" 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): exit_code = os.system(cmd) @@ -282,10 +287,10 @@ def generate_control_file(version): Architecture: %s Maintainer: rustdesk 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, libappindicator3-1, gstreamer1.0-pipewire%s Description: A remote control software. -""" % (version, get_deb_arch()) +""" % (version, get_deb_arch(), get_deb_extra_depends()) file = open(control_file_path, "w") file.write(content) file.close() From a103b83647f31a4bc2e572fb7d760bf4d0508e1c Mon Sep 17 00:00:00 2001 From: FastAct <93490087+FastAct@users.noreply.github.com> Date: Tue, 30 Jul 2024 14:23:49 +0200 Subject: [PATCH 028/541] Update nl.rs (#8892) --- src/lang/nl.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lang/nl.rs b/src/lang/nl.rs index 4f4fbcc2bcc8..eceb1b6ff19e 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -631,6 +631,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("cancel-bot-confirm-tip", "Weet je zeker dat je de Telegram-bot wilt annuleren?"), ("About RustDesk", "Over RustDesk"), ("Send clipboard keystrokes", "Klembord toetsaanslagen verzenden"), - ("network_error_tip", ""), + ("network_error_tip", "Controleer de netwerkverbinding en selecteer 'Opnieuw proberen'."), ].iter().cloned().collect(); } From 35571dc8d76821e2ba217fe124bafbe04fc9e60e Mon Sep 17 00:00:00 2001 From: solokot Date: Wed, 31 Jul 2024 09:16:55 +0300 Subject: [PATCH 029/541] Update ru.rs (#8901) --- src/lang/ru.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 7ccf6ba6f9f7..0c8a24718ee7 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -629,8 +629,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("enable-bot-desc", "1) Откройте чат с @BotFather.\n2) Отправьте команду \"/newbot\". После выполнения этого шага вы получите токен.\n3) Начните чат с вашим только что созданным ботом. Отправьте сообщение, начинающееся с прямой косой черты (\"/\"), например, \"/hello\", чтобы его активировать.\n"), ("cancel-2fa-confirm-tip", "Отключить двухфакторную аутентификацию?"), ("cancel-bot-confirm-tip", "Отключить Telegram-бота?"), - ("About RustDesk", ""), - ("Send clipboard keystrokes", ""), - ("network_error_tip", ""), + ("About RustDesk", "О RustDesk"), + ("Send clipboard keystrokes", "Отправлять нажатия клавиш из буфера обмена"), + ("network_error_tip", "Проверьте подключение к сети, затем нажмите \"Повтор\"."), ].iter().cloned().collect(); } From 4fec8abad41d1e5e4535f37e2802d2b15dfd35a0 Mon Sep 17 00:00:00 2001 From: 21pages Date: Wed, 31 Jul 2024 17:25:10 +0800 Subject: [PATCH 030/541] update repo to rustdesk-org (#8905) Signed-off-by: 21pages --- Cargo.lock | 16 ++++++++-------- Cargo.toml | 6 +++--- flutter/pubspec.lock | 4 ++-- flutter/pubspec.yaml | 4 ++-- libs/hbb_common/Cargo.toml | 2 +- libs/scrap/Cargo.toml | 4 ++-- 6 files changed, 18 insertions(+), 18 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7a51371b5a75..ca6bffe1f992 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -123,7 +123,7 @@ checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" [[package]] name = "android-wakelock" version = "0.1.0" -source = "git+https://github.com/21pages/android-wakelock#d0292e5a367e627c4fa6f1ca6bdfad005dca7d90" +source = "git+https://github.com/rustdesk-org/android-wakelock#d0292e5a367e627c4fa6f1ca6bdfad005dca7d90" dependencies = [ "jni 0.21.1", "log", @@ -3044,8 +3044,8 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hwcodec" -version = "0.5.1" -source = "git+https://github.com/21pages/hwcodec#74e8288f776a9d43861f16aa62e86b57c7209868" +version = "0.7.0" +source = "git+https://github.com/rustdesk-org/hwcodec#97522dfcd10b46af4c2ac1a66b769d69f8fd582a" dependencies = [ "bindgen 0.59.2", "cc", @@ -3173,7 +3173,7 @@ dependencies = [ [[package]] name = "impersonate_system" version = "0.1.0" -source = "git+https://github.com/21pages/impersonate-system#2f429010a5a10b1fe5eceb553c6672fd53d20167" +source = "git+https://github.com/rustdesk-org/impersonate-system#2f429010a5a10b1fe5eceb553c6672fd53d20167" dependencies = [ "cc", ] @@ -3674,7 +3674,7 @@ dependencies = [ [[package]] name = "machine-uid" version = "0.3.0" -source = "git+https://github.com/21pages/machine-uid#381ff579c1dc3a6c54db9dfec47c44bcb0246542" +source = "git+https://github.com/rustdesk-org/machine-uid#381ff579c1dc3a6c54db9dfec47c44bcb0246542" dependencies = [ "bindgen 0.59.2", "cc", @@ -7050,7 +7050,7 @@ dependencies = [ [[package]] name = "wallpaper" version = "3.2.0" -source = "git+https://github.com/21pages/wallpaper.rs#ce4a0cd3f58327c7cc44d15a63706fb0c022bacf" +source = "git+https://github.com/rustdesk-org/wallpaper.rs#ce4a0cd3f58327c7cc44d15a63706fb0c022bacf" dependencies = [ "dirs 5.0.1", "enquote", @@ -7239,7 +7239,7 @@ dependencies = [ [[package]] name = "webm" version = "1.1.0" -source = "git+https://github.com/21pages/rust-webm#d2c4d3ac133c7b0e4c0f656da710b48391981e64" +source = "git+https://github.com/rustdesk-org/rust-webm#d2c4d3ac133c7b0e4c0f656da710b48391981e64" dependencies = [ "webm-sys", ] @@ -7247,7 +7247,7 @@ dependencies = [ [[package]] name = "webm-sys" version = "1.0.4" -source = "git+https://github.com/21pages/rust-webm#d2c4d3ac133c7b0e4c0f656da710b48391981e64" +source = "git+https://github.com/rustdesk-org/rust-webm#d2c4d3ac133c7b0e4c0f656da710b48391981e64" dependencies = [ "cc", ] diff --git a/Cargo.toml b/Cargo.toml index 2879ba91f090..5a052af0dfd0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -114,7 +114,7 @@ winapi = { version = "0.3", features = [ winreg = "0.11" windows-service = "0.6" 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" tauri-winrt-notification = "0.1.2" runas = "1.2" @@ -138,7 +138,7 @@ image = "0.24" keepawake = { git = "https://github.com/rustdesk-org/keepawake-rs" } [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] # https://github.com/rustdesk/rustdesk-server-pro/issues/189, using native-tls for better tls support @@ -165,7 +165,7 @@ once_cell = {version = "1.18", optional = true} [target.'cfg(target_os = "android")'.dependencies] android_logger = "0.13" jni = "0.21" -android-wakelock = { git = "https://github.com/21pages/android-wakelock" } +android-wakelock = { git = "https://github.com/rustdesk-org/android-wakelock" } [workspace] members = ["libs/scrap", "libs/hbb_common", "libs/enigo", "libs/clipboard", "libs/virtual_display", "libs/virtual_display/dylib", "libs/portable"] diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index 060606181bbb..72eaeabc22a5 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -377,7 +377,7 @@ packages: path: "." ref: "24cb88413fa5181d949ddacbb30a65d5c459e7d9" resolved-ref: "24cb88413fa5181d949ddacbb30a65d5c459e7d9" - url: "https://github.com/21pages/dynamic_layouts.git" + url: "https://github.com/rustdesk-org/dynamic_layouts.git" source: git version: "0.0.1+1" external_path: @@ -511,7 +511,7 @@ packages: path: "." ref: "38951317afe79d953ab25733667bd96e172a80d3" resolved-ref: "38951317afe79d953ab25733667bd96e172a80d3" - url: "https://github.com/21pages/flutter_gpu_texture_renderer" + url: "https://github.com/rustdesk-org/flutter_gpu_texture_renderer" source: git version: "0.0.1" flutter_improved_scrolling: diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index a0a24d54d7e6..965e7f7bd16d 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -92,14 +92,14 @@ dependencies: dropdown_button2: ^2.0.0 flutter_gpu_texture_renderer: git: - url: https://github.com/21pages/flutter_gpu_texture_renderer + url: https://github.com/rustdesk-org/flutter_gpu_texture_renderer ref: 38951317afe79d953ab25733667bd96e172a80d3 uuid: ^3.0.7 auto_size_text_field: ^2.2.1 flex_color_picker: ^3.3.0 dynamic_layouts: git: - url: https://github.com/21pages/dynamic_layouts.git + url: https://github.com/rustdesk-org/dynamic_layouts.git ref: 24cb88413fa5181d949ddacbb30a65d5c459e7d9 pull_down_button: ^0.9.3 device_info_plus: ^9.1.0 diff --git a/libs/hbb_common/Cargo.toml b/libs/hbb_common/Cargo.toml index 7aeb43797764..048c8075fbb7 100644 --- a/libs/hbb_common/Cargo.toml +++ b/libs/hbb_common/Cargo.toml @@ -46,7 +46,7 @@ url = "2.2" [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] mac_address = "1.1" -machine-uid = { git = "https://github.com/21pages/machine-uid" } +machine-uid = { git = "https://github.com/rustdesk-org/machine-uid" } [target.'cfg(not(any(target_os = "macos", target_os = "windows")))'.dependencies] tokio-rustls = { version = "0.26", features = ["logging", "tls12", "ring"], default-features = false } rustls-platform-verifier = "0.3.1" diff --git a/libs/scrap/Cargo.toml b/libs/scrap/Cargo.toml index 64805e0e5082..3a0784e7cca4 100644 --- a/libs/scrap/Cargo.toml +++ b/libs/scrap/Cargo.toml @@ -21,7 +21,7 @@ cfg-if = "1.0" num_cpus = "1.15" lazy_static = "1.4" hbb_common = { path = "../hbb_common" } -webm = { git = "https://github.com/21pages/rust-webm" } +webm = { git = "https://github.com/rustdesk-org/rust-webm" } serde = {version="1.0", features=["derive"]} [dependencies.winapi] @@ -59,7 +59,7 @@ gstreamer-app = { version = "0.16", features = ["v1_10"], optional = true } gstreamer-video = { version = "0.16", optional = true } [dependencies.hwcodec] -git = "https://github.com/21pages/hwcodec" +git = "https://github.com/rustdesk-org/hwcodec" optional = true From cb6a6aa42a3e42a8858b45b8a93cea376fd5dadb Mon Sep 17 00:00:00 2001 From: Vasyl Gello Date: Wed, 31 Jul 2024 12:38:00 +0000 Subject: [PATCH 031/541] flutter-build: Parameterize Android build matrix (#8907) As @rustdesk noted debug builds are no-go in official RD repo but this change makes it possible to filter jobs only relevant to Android from flutter-build.yml to build only Android in a separate mirror of Rustdesk. Signed-off-by: Vasyl Gello --- .github/workflows/flutter-build.yml | 57 +++++++++++++++++++++++++---- flutter/ndk_x86.sh | 2 + 2 files changed, 52 insertions(+), 7 deletions(-) create mode 100755 flutter/ndk_x86.sh diff --git a/.github/workflows/flutter-build.yml b/.github/workflows/flutter-build.yml index 0e2b450639cf..6439764a63bb 100644 --- a/.github/workflows/flutter-build.yml +++ b/.github/workflows/flutter-build.yml @@ -776,13 +776,22 @@ jobs: arch: aarch64, target: aarch64-linux-android, os: ubuntu-20.04, - openssl-arch: android-arm64, + reltype: release, + suffix: "", } - { arch: armv7, target: armv7-linux-androideabi, os: ubuntu-20.04, - openssl-arch: android-arm, + reltype: release, + suffix: "", + } + - { + arch: x86_64, + target: x86_64-linux-android, + os: ubuntu-20.04, + reltype: release, + suffix: "", } steps: - name: Export GitHub Actions cache environment variables @@ -855,6 +864,12 @@ jobs: armv7-linux-androideabi) ./flutter/build_android_deps.sh armeabi-v7a ;; + x86_64-linux-android) + ./flutter/build_android_deps.sh x86_64 + ;; + i686-linux-android) + ./flutter/build_android_deps.sh x86 + ;; esac shell: bash @@ -900,6 +915,16 @@ jobs: 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 ;; + x86_64-linux-android) + ./flutter/ndk_x64.sh + mkdir -p ./flutter/android/app/src/main/jniLibs/x86_64 + cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/x86_64/librustdesk.so + ;; + i686-linux-android) + ./flutter/ndk_x86.sh + mkdir -p ./flutter/android/app/src/main/jniLibs/x86 + cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/x86/librustdesk.so + ;; esac - name: Build rustdesk @@ -917,8 +942,8 @@ jobs: 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 + flutter build apk "--${{ matrix.job.reltype }}" --target-platform android-arm64 --split-per-abi + mv build/app/outputs/flutter-apk/app-arm64-v8a-${{ matrix.job.reltype }}.apk ../rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}${{ matrix.job.suffix }}.apk ;; armv7-linux-androideabi) mkdir -p ./flutter/android/app/src/main/jniLibs/armeabi-v7a @@ -926,13 +951,31 @@ jobs: 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 + flutter build apk "--${{ matrix.job.reltype }}" --target-platform android-arm --split-per-abi + mv build/app/outputs/flutter-apk/app-armeabi-v7a-${{ matrix.job.reltype }}.apk ../rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}${{ matrix.job.suffix }}.apk + ;; + x86_64-linux-android) + mkdir -p ./flutter/android/app/src/main/jniLibs/x86_64 + cp ${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/x86_64-linux-android/libc++_shared.so ./flutter/android/app/src/main/jniLibs/x86_64/ + cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/x86_64/librustdesk.so + # build flutter + pushd flutter + flutter build apk "--${{ matrix.job.reltype }}" --target-platform android-x64 --split-per-abi + mv build/app/outputs/flutter-apk/app-x86_64-${{ matrix.job.reltype }}.apk ../rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}${{ matrix.job.suffix }}.apk + ;; + i686-linux-android) + mkdir -p ./flutter/android/app/src/main/jniLibs/x86 + cp ${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/i686-linux-android/libc++_shared.so ./flutter/android/app/src/main/jniLibs/x86/ + cp ./target/${{ matrix.job.target }}/debug/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/x86/librustdesk.so + # build flutter + pushd flutter + flutter build apk "--${{ matrix.job.reltype }}" --target-platform android-x86 --split-per-abi + mv build/app/outputs/flutter-apk/app-x86-${{ matrix.job.reltype }}.apk ../rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}${{ matrix.job.suffix }}.apk ;; esac popd mkdir -p signed-apk; pushd signed-apk - mv ../rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.apk . + mv ../rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}${{ matrix.job.suffix }}.apk . - uses: r0adkll/sign-android-release@v1 name: Sign app APK diff --git a/flutter/ndk_x86.sh b/flutter/ndk_x86.sh new file mode 100755 index 000000000000..617c25f65de4 --- /dev/null +++ b/flutter/ndk_x86.sh @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +cargo ndk --platform 21 --target i686-linux-android build --release --features flutter From 9f91eada898d28d13bd7707190aeb7936dcf02e1 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Thu, 1 Aug 2024 09:09:45 +0800 Subject: [PATCH 032/541] fix https://github.com/rustdesk/rustdesk-server-pro/issues/338 --- res/devices.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/devices.py b/res/devices.py index 4259abd41623..d72614b70465 100755 --- a/res/devices.py +++ b/res/devices.py @@ -46,7 +46,7 @@ def view( devices.append(device) continue last_online = datetime.strptime( - device["last_online"], "%Y-%m-%dT%H:%M:%S" + device["last_online"].split(".")[0], "%Y-%m-%dT%H:%M:%S" ) # assuming date is in this format if (datetime.utcnow() - last_online).days >= offline_days: devices.append(device) From a12969be307909d3540694e5d07b424ae2b97d95 Mon Sep 17 00:00:00 2001 From: Hamir Mahal Date: Wed, 31 Jul 2024 18:12:11 -0700 Subject: [PATCH 033/541] fix: usage of `actions/checkout@v3` (#8912) * chore: changes from formatting on save * fix: usage of `actions/checkout@v3` --- .github/workflows/flutter-build.yml | 66 +++++++++++++++-------------- 1 file changed, 35 insertions(+), 31 deletions(-) diff --git a/.github/workflows/flutter-build.yml b/.github/workflows/flutter-build.yml index 6439764a63bb..87c4f78783d1 100644 --- a/.github/workflows/flutter-build.yml +++ b/.github/workflows/flutter-build.yml @@ -58,7 +58,7 @@ jobs: fail-fast: false build-for-windows-flutter: - name: ${{ matrix.job.target }} + name: ${{ matrix.job.target }} needs: [build-RustDeskTempTopMostWindow] runs-on: ${{ matrix.job.os }} strategy: @@ -67,7 +67,12 @@ jobs: job: # - { target: i686-pc-windows-msvc , os: windows-2022 } # - { target: x86_64-pc-windows-gnu , os: windows-2022 } - - { target: x86_64-pc-windows-msvc, os: windows-2022, arch: x86_64, vcpkg-triplet: x64-windows-static } + - { + target: x86_64-pc-windows-msvc, + os: windows-2022, + arch: x86_64, + vcpkg-triplet: x64-windows-static, + } # - { target: aarch64-pc-windows-msvc, os: windows-2022, arch: aarch64 } steps: - name: Export GitHub Actions cache environment variables @@ -160,7 +165,7 @@ jobs: if: env.UPLOAD_ARTIFACT == 'true' uses: actions/upload-artifact@master with: - name: rustdesk-unsigned-windows-${{ matrix.job.arch }} + name: rustdesk-unsigned-windows-${{ matrix.job.arch }} path: rustdesk - name: Sign rustdesk files @@ -223,7 +228,12 @@ jobs: job: # - { target: i686-pc-windows-msvc , os: windows-2022 } # - { target: x86_64-pc-windows-gnu , os: windows-2022 } - - { target: i686-pc-windows-msvc, os: windows-2022, arch: x86, vcpkg-triplet: x86-windows-static } + - { + target: i686-pc-windows-msvc, + os: windows-2022, + arch: x86, + vcpkg-triplet: x86-windows-static, + } # - { target: aarch64-pc-windows-msvc, os: windows-2022 } steps: - name: Export GitHub Actions cache environment variables @@ -234,7 +244,7 @@ jobs: core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || ''); - name: Checkout source code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install LLVM and Clang uses: rustdesk-org/install-llvm-action-32bit@master @@ -282,7 +292,7 @@ jobs: # Do not remove x64 files, because the user may run the 32bit version on a 64bit system. # Do not remove ./usbmmidd_v2/deviceinstaller64.exe, as x86 exe cannot install and uninstall drivers when running on x64, # we need the x64 exe to install and uninstall the driver. - rm -rf ./usbmmidd_v2/deviceinstaller.exe ./usbmmidd_v2/usbmmidd.bat + rm -rf ./usbmmidd_v2/deviceinstaller.exe ./usbmmidd_v2/usbmmidd.bat mv ./usbmmidd_v2 ./Release || true - name: find Runner.res @@ -347,7 +357,7 @@ jobs: core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || ''); - name: Checkout source code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install flutter rust bridge deps shell: bash @@ -432,7 +442,7 @@ jobs: run: | brew install nasm yasm - name: Checkout source code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install flutter uses: subosito/flutter-action@v2 with: @@ -512,7 +522,7 @@ jobs: core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || ''); - name: Checkout source code - uses: actions/checkout@v3 + uses: actions/checkout@v4 # $VCPKG_ROOT/vcpkg install --triplet arm64-ios --x-install-root="$VCPKG_ROOT/installed" @@ -582,7 +592,7 @@ jobs: core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || ''); - name: Checkout source code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Import the codesign cert if: env.MACOS_P12_BASE64 != null @@ -837,7 +847,7 @@ jobs: wget - name: Checkout source code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install flutter uses: subosito/flutter-action@v2 with: @@ -1018,7 +1028,7 @@ jobs: build-rustdesk-linux: needs: [generate-bridge-linux] - name: build rustdesk linux ${{ matrix.job.target }} + name: build rustdesk linux ${{ matrix.job.target }} runs-on: ${{ matrix.job.on }} strategy: fail-fast: false @@ -1059,7 +1069,7 @@ jobs: sudo apt-get install -y nasm qemu-user-static - name: Checkout source code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set Swap Space if: ${{ matrix.job.arch == 'x86_64' }} @@ -1286,16 +1296,16 @@ jobs: rustdesk-*.deb rustdesk-*.rpm - - name: Upload deb + - name: Upload deb uses: actions/upload-artifact@master if: env.UPLOAD_ARTIFACT == 'true' with: name: rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.deb path: rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.deb - + # only x86_64 for arch since we can not find newest arm64 docker image to build # old arch image does not make sense for arch since it is "arch" which always update to date - # and failed to makepkg arm64 on x86_64 + # and failed to makepkg arm64 on x86_64 - name: Patch archlinux PKGBUILD if: matrix.job.arch == 'x86_64' && env.UPLOAD_ARTIFACT == 'true' run: | @@ -1360,7 +1370,7 @@ jobs: core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || ''); - name: Checkout source code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Free Space run: | @@ -1519,7 +1529,7 @@ jobs: files: | rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}-sciter.deb - - name: Upload deb + - name: Upload deb uses: actions/upload-artifact@master if: env.UPLOAD_ARTIFACT == 'true' with: @@ -1535,17 +1545,11 @@ jobs: fail-fast: false matrix: job: - - { - target: x86_64-unknown-linux-gnu, - arch: x86_64, - } - - { - target: aarch64-unknown-linux-gnu, - arch: aarch64, - } + - { target: x86_64-unknown-linux-gnu, arch: x86_64 } + - { target: aarch64-unknown-linux-gnu, arch: aarch64 } steps: - name: Checkout source code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Download Binary uses: actions/download-artifact@master @@ -1584,7 +1588,7 @@ jobs: build-flatpak: name: Build flatpak ${{ matrix.job.target }}${{ matrix.job.suffix }} - needs: + needs: - build-rustdesk-linux - build-rustdesk-linux-sciter runs-on: ${{ matrix.job.on }} @@ -1617,7 +1621,7 @@ jobs: } steps: - name: Checkout source code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Download Binary uses: actions/download-artifact@master @@ -1652,7 +1656,7 @@ jobs: # disable git safe.directory git config --global --add safe.directory "*" pushd /workspace - # install + # install apt-get update -y apt-get install -y \ cmake \ @@ -1693,7 +1697,7 @@ jobs: RELEASE_NAME: web-basic steps: - name: Checkout source code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Prepare env run: | From 85604dee7915122646efd378486bc6905ef844bf Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Thu, 1 Aug 2024 15:08:52 +0800 Subject: [PATCH 034/541] refact: msi, custom client props (#8913) * refact: msi, custom client props Signed-off-by: fufesou * format Signed-off-by: fufesou --------- Signed-off-by: fufesou --- res/msi/Package/Components/RustDesk.wxs | 14 ++++---- .../Package/Fragments/AddRemoveProperties.wxs | 3 ++ res/msi/preprocess.py | 36 +++++++++++++++++++ 3 files changed, 46 insertions(+), 7 deletions(-) diff --git a/res/msi/Package/Components/RustDesk.wxs b/res/msi/Package/Components/RustDesk.wxs index fa8c402e1f52..b2049f4e8a02 100644 --- a/res/msi/Package/Components/RustDesk.wxs +++ b/res/msi/Package/Components/RustDesk.wxs @@ -17,7 +17,7 @@ - + @@ -40,11 +40,11 @@ - + - - + + @@ -54,14 +54,14 @@ - + - + @@ -109,7 +109,7 @@ - + diff --git a/res/msi/Package/Fragments/AddRemoveProperties.wxs b/res/msi/Package/Fragments/AddRemoveProperties.wxs index f93852867a17..a7139fab2c90 100644 --- a/res/msi/Package/Fragments/AddRemoveProperties.wxs +++ b/res/msi/Package/Fragments/AddRemoveProperties.wxs @@ -25,6 +25,9 @@ + + + diff --git a/res/msi/preprocess.py b/res/msi/preprocess.py index 02db5bde43ab..9a6a3bcab874 100644 --- a/res/msi/preprocess.py +++ b/res/msi/preprocess.py @@ -64,6 +64,12 @@ def make_parser(): parser.add_argument( "-c", "--custom", action="store_true", help="Is custom client", default=False ) + parser.add_argument( + "--custom-client-props", + type=str, + default="{}", + help='Custom client properties, e.g. \'{"connection-type": "outgoing"}\'', + ) parser.add_argument( "--app-name", type=str, default="RustDesk", help="The app name." ) @@ -385,6 +391,33 @@ def gen_custom_ARPSYSTEMCOMPONENT(args, dist_dir): else: return gen_custom_ARPSYSTEMCOMPONENT_False(args) +def gen_custom_client_properties(args): + try: + props = json.loads(args.custom_client_props) + except json.JSONDecodeError as e: + print(f"Failed to decode custom props: {e}") + return False + + def func(lines, index_start): + indent = g_indent_unit * 3 + + lines_new = [] + + if 'connection-type' in props: + lines_new.append( + f"""{indent}\n""" + ) + + for i, line in enumerate(lines_new): + lines.insert(index_start + i + 1, line) + return lines + + return gen_content_between_tags( + "Package/Fragments/AddRemoveProperties.wxs", + "", + "", + func, + ) def gen_content_between_tags(filename, tag_start, tag_end, func): target_file = Path(sys.argv[0]).parent.joinpath(filename) @@ -506,6 +539,9 @@ def replace_component_guids_in_wxs(): if not gen_custom_ARPSYSTEMCOMPONENT(args, dist_dir): sys.exit(-1) + if not gen_custom_client_properties(args): + sys.exit(-1) + if not gen_auto_component(app_name, dist_dir): sys.exit(-1) From 1707987a7ba27c1a4b43c436256886576d414ca6 Mon Sep 17 00:00:00 2001 From: Vasyl Gello Date: Thu, 1 Aug 2024 07:09:33 +0000 Subject: [PATCH 035/541] Fix typo in android-x86 lib copy step (#8914) Signed-off-by: Vasyl Gello --- .github/workflows/flutter-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/flutter-build.yml b/.github/workflows/flutter-build.yml index 87c4f78783d1..f88091959553 100644 --- a/.github/workflows/flutter-build.yml +++ b/.github/workflows/flutter-build.yml @@ -976,7 +976,7 @@ jobs: i686-linux-android) mkdir -p ./flutter/android/app/src/main/jniLibs/x86 cp ${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/i686-linux-android/libc++_shared.so ./flutter/android/app/src/main/jniLibs/x86/ - cp ./target/${{ matrix.job.target }}/debug/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/x86/librustdesk.so + cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/x86/librustdesk.so # build flutter pushd flutter flutter build apk "--${{ matrix.job.reltype }}" --target-platform android-x86 --split-per-abi From f6aca4ca8e3eb7661f6665540494278fba7ce87a Mon Sep 17 00:00:00 2001 From: 21pages Date: Thu, 1 Aug 2024 18:17:02 +0800 Subject: [PATCH 036/541] fix click cm audio permission button before auth (#8917) 1. When not authenticated, clicking the audio permission button on the cm will send audio data 2. Keep the cursor position code unchanged, because `show_remote_cursor` is false before auth, so subscription will not happen. 3. Keep the clipboard code unchanged, because the keyboard permission will also be determined in `try_sub_services`. If the clipboard permission is clicked before auth and the keyboard permission is clicked after auth, the clipboard service will not be subscribed. Signed-off-by: 21pages --- src/server/connection.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/server/connection.rs b/src/server/connection.rs index 5ea1e923a81b..b1af36e47c51 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -502,10 +502,12 @@ impl Connection { } else if &name == "audio" { conn.audio = enabled; conn.send_permission(Permission::Audio, enabled).await; - if let Some(s) = conn.server.upgrade() { - s.write().unwrap().subscribe( - super::audio_service::NAME, - conn.inner.clone(), conn.audio_enabled()); + if conn.authorized { + if let Some(s) = conn.server.upgrade() { + s.write().unwrap().subscribe( + super::audio_service::NAME, + conn.inner.clone(), conn.audio_enabled()); + } } } else if &name == "file" { conn.file = enabled; From 61ccc2152ed06a14f77da0c6988215cf44fc7098 Mon Sep 17 00:00:00 2001 From: Vasyl Gello Date: Thu, 1 Aug 2024 13:33:59 +0000 Subject: [PATCH 037/541] Bump Android NDK to r27 (#8918) Signed-off-by: Vasyl Gello --- .github/workflows/flutter-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/flutter-build.yml b/.github/workflows/flutter-build.yml index f88091959553..6ca9d13609d3 100644 --- a/.github/workflows/flutter-build.yml +++ b/.github/workflows/flutter-build.yml @@ -34,7 +34,7 @@ env: # vcpkg version: 2024.07.12 VCPKG_COMMIT_ID: "1de2026f28ead93ff1773e6e680387643e914ea1" VERSION: "1.3.0" - NDK_VERSION: "r26d" + NDK_VERSION: "r27" #signing keys env variable checks ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}" MACOS_P12_BASE64: "${{ secrets.MACOS_P12_BASE64 }}" From 2333ee2c07c6a67ed6c58ccfbc50a3bee203f90d Mon Sep 17 00:00:00 2001 From: rustdesk Date: Fri, 2 Aug 2024 09:29:27 +0800 Subject: [PATCH 038/541] fix https://github.com/rustdesk/rustdesk/issues/8923 --- src/lang.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lang.rs b/src/lang.rs index b269d8631fc0..d66b3895661e 100644 --- a/src/lang.rs +++ b/src/lang.rs @@ -72,7 +72,7 @@ pub const LANGS: &[(&str, &str)] = &[ ("ja", "日本語"), ("ko", "한국어"), ("kz", "Қазақ"), - ("ua", "Українська"), + ("uk", "Українська"), ("fa", "فارسی"), ("ca", "Català"), ("el", "Ελληνικά"), @@ -144,7 +144,7 @@ pub fn translate_locale(name: String, locale: &str) -> String { "ja" => ja::T.deref(), "ko" => ko::T.deref(), "kz" => kz::T.deref(), - "ua" => ua::T.deref(), + "uk" => ua::T.deref(), "fa" => fa::T.deref(), "ca" => ca::T.deref(), "el" => el::T.deref(), From e67b694f06afa40a754993c0747916d5a596e1dd Mon Sep 17 00:00:00 2001 From: Yevhen Popok Date: Fri, 2 Aug 2024 04:31:37 +0300 Subject: [PATCH 039/541] Update Ukrainian translation (#8921) --- src/lang/ua.rs | 82 +++++++++++++++++++++++++------------------------- 1 file changed, 41 insertions(+), 41 deletions(-) diff --git a/src/lang/ua.rs b/src/lang/ua.rs index 187603fb08c7..c9cee27e4009 100644 --- a/src/lang/ua.rs +++ b/src/lang/ua.rs @@ -3,12 +3,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = [ ("Status", "Статус"), ("Your Desktop", "Ваша стільниця"), - ("desk_tip", "Ваша стільниця доступна з цим ідентифікатором і паролем"), + ("desk_tip", "Доступ до вашої стільниці можливий з цим ID та паролем."), ("Password", "Пароль"), ("Ready", "Готово"), ("Established", "Встановлено"), ("connecting_status", "Підключення до мережі RustDesk..."), - ("Enable service", "Включити службу"), + ("Enable service", "Увімкнути службу"), ("Start service", "Запустити службу"), ("Service is running", "Служба працює"), ("Service is not running", "Служба не запущена"), @@ -41,9 +41,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("length %min% to %max%", "від %min% до %max% символів"), ("starts with a letter", "починається з літери"), ("allowed characters", "дозволені символи"), - ("id_change_tip", "Допускаються лише символи a-z, A-Z, 0-9 і _ (підкреслення). Першою повинна бути літера a-z, A-Z. В межах від 6 до 16 символів"), + ("id_change_tip", "Допускаються лише символи a-z, A-Z, 0-9 і _ (підкреслення). Першою повинна бути літера a-z, A-Z. Довжина — від 6 до 16 символів"), ("Website", "Веб-сайт"), - ("About", "Про RustDesk"), + ("About", "Про застосунок"), ("Slogan_tip", "Створено з душею в цьому хаотичному світі!"), ("Privacy Statement", "Декларація про конфіденційність"), ("Mute", "Вимкнути звук"), @@ -66,7 +66,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Cancel", "Скасувати"), ("Skip", "Пропустити"), ("Close", "Закрити"), - ("Retry", "Спробувати знову"), + ("Retry", "Повторити"), ("OK", "OK"), ("Password Required", "Потрібен пароль"), ("Please enter your password", "Будь ласка, введіть ваш пароль"), @@ -145,23 +145,23 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Failed to make direct connection to remote desktop", "Не вдалося встановити пряме підключення до віддаленої стільниці"), ("Set Password", "Встановити пароль"), ("OS Password", "Пароль ОС"), - ("install_tip", "Через UAC в деяких випадках RustDesk може працювати некоректно на віддаленому вузлі. Щоб уникнути UAC, натисніть кнопку нижче для встановлення RustDesk в системі"), + ("install_tip", "Через UAC, в деяких випадках RustDesk може працювати некоректно на віддаленому вузлі. Щоб уникнути UAC, натисніть кнопку нижче для встановлення RustDesk в системі"), ("Click to upgrade", "Натисніть, щоб перевірити наявність оновлень"), ("Click to download", "Натисніть, щоб завантажити"), ("Click to update", "Натисніть, щоб оновити"), ("Configure", "Налаштувати"), - ("config_acc", "Для віддаленого керування вашою стільницею, вам необхідно надати RustDesk дозволи \"Доступності\""), - ("config_screen", "Для віддаленого доступу до вашої стільниці,вам необхідно надати RustDesk дозволи на \"Запис екрана\""), + ("config_acc", "Для віддаленого керування вашою стільницею, вам необхідно надати RustDesk дозволи \"Спеціальні можливості\""), + ("config_screen", "Для віддаленого доступу до вашої стільниці, вам необхідно надати RustDesk дозволи на \"Запис екрана\""), ("Installing ...", "Встановлюється..."), ("Install", "Встановити"), ("Installation", "Встановлення"), ("Installation Path", "Шлях встановлення"), ("Create start menu shortcuts", "Створити ярлики меню \"Пуск\""), - ("Create desktop icon", "Створити значок на стільниці"), + ("Create desktop icon", "Створити піктограму на стільниці"), ("agreement_tip", "Починаючи встановлення, ви приймаєте умови ліцензійної угоди"), ("Accept and Install", "Прийняти та встановити"), ("End-user license agreement", "Ліцензійна угода з кінцевим користувачем"), - ("Generating ...", "Генерація..."), + ("Generating ...", "Генерування..."), ("Your installation is lower version.", "У вас встановлена більш рання версія"), ("not_close_tcp_tip", "Не закривайте це вікно під час використання тунелю"), ("Listening ...", "Очікуємо ..."), @@ -173,7 +173,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Local Address", "Локальна адреса"), ("Change Local Port", "Змінити локальний порт"), ("setup_server_tip", "Для пришвидшення зʼєднання, будь ласка, налаштуйте власний сервер"), - ("Too short, at least 6 characters.", "Занадто коротко, мінімум 6 символів"), + ("Too short, at least 6 characters.", "Має бути щонайменше 6 символів"), ("The confirmation is not identical.", "Підтвердження не збігається"), ("Permissions", "Дозволи"), ("Accept", "Прийняти"), @@ -188,8 +188,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enter Remote ID", "Введіть віддалений ID"), ("Enter your password", "Введіть пароль"), ("Logging in...", "Вхід..."), - ("Enable RDP session sharing", "Включити загальний доступ до сеансу RDP"), - ("Auto Login", "Автоматичний вхід (дійсний, тільки якщо ви встановили \"Завершення користувацького сеансу після завершення віддаленого підключення\")"), + ("Enable RDP session sharing", "Увімкнути загальний доступ до сеансу RDP"), + ("Auto Login", "Автоматичний вхід (дійсний лише якщо ви встановили \"Блокування після завершення сеансу\")"), ("Enable direct IP access", "Увімкнути прямий IP-доступ"), ("Rename", "Перейменувати"), ("Space", "Місце"), @@ -212,17 +212,17 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Run without install", "Запустити без встановлення"), ("Connect via relay", "Підключитися через ретранслятор"), ("Always connect via relay", "Завжди підключатися через ретранслятор"), - ("whitelist_tip", "Тільки IP-адреси з білого списку можуть отримати доступ до мене"), + ("whitelist_tip", "Лише IP-адреси з білого списку можуть отримати доступ до мене"), ("Login", "Увійти"), ("Verify", "Підтвердити"), ("Remember me", "Запамʼятати мене"), ("Trust this device", "Довірений пристрій"), ("Verification code", "Код підтвердження"), - ("verification_tip", "Виявлено новий пристрій, код підтвердження надіслано на зареєстровану email-адресу, введіть код підтвердження для продовження авторизації."), + ("verification_tip", "Код підтвердження надіслано на зареєстровану email-адресу, введіть код підтвердження для продовження авторизації."), ("Logout", "Вийти"), ("Tags", "Теги"), ("Search ID", "Пошук за ID"), - ("whitelist_sep", "Розділені комою, крапкою з комою, пробілом або новим рядком"), + ("whitelist_sep", "Відокремлення комою, крапкою з комою, пропуском або новим рядком"), ("Add ID", "Додати ID"), ("Add Tag", "Додати ключове слово"), ("Unselect all tags", "Скасувати вибір усіх тегів"), @@ -253,7 +253,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Left Mouse", "Ліва кнопка миші"), ("One-Long Tap", "Одне довге натискання пальцем"), ("Two-Finger Tap", "Дотик двома пальцями"), - ("Right Mouse", "Права миша"), + ("Right Mouse", "Права кнопка миші"), ("One-Finger Move", "Рух одним пальцем"), ("Double Tap & Move", "Подвійне натискання та переміщення"), ("Mouse Drag", "Перетягування мишею"), @@ -280,14 +280,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Do you accept?", "Ви згодні?"), ("Open System Setting", "Відкрити налаштування системи"), ("How to get Android input permission?", "Як отримати дозвіл на введення Android?"), - ("android_input_permission_tip1", "Для того, щоб віддалений пристрій міг керувати вашим Android-пристроєм за допомогою миші або торкання, вам необхідно дозволити RustDesk використовувати службу \"Спеціальні можливості\"."), - ("android_input_permission_tip2", "Перейдіть на наступну сторінку системних налаштувань, знайдіть та увійдіть у [Встановлені служби], увімкніть службу [RustDesk Input]."), + ("android_input_permission_tip1", "Для того, щоб віддалений пристрій міг керувати вашим Android-пристроєм за допомогою миші або дотику, вам необхідно дозволити RustDesk використовувати службу \"Спеціальні можливості\"."), + ("android_input_permission_tip2", "Будь ласка, перейдіть на наступну сторінку системних налаштувань, знайдіть та увійдіть у [Встановлені служби], увімкніть службу [RustDesk Input]."), ("android_new_connection_tip", "Отримано новий запит на керування вашим поточним пристроєм."), - ("android_service_will_start_tip", "Увімкнення захоплення екрана автоматично запускає службу, дозволяючи іншим пристроям запитувати підключення до вашого пристрою."), + ("android_service_will_start_tip", "Увімкнення \"Захоплення екрана\" автоматично запускає службу, дозволяючи іншим пристроям запитувати підключення до вашого пристрою."), ("android_stop_service_tip", "Зупинка служби автоматично завершить всі встановлені зʼєднання."), - ("android_version_audio_tip", "Поточна версія Android не підтримує захоплення звуку, оновіть її до Android 10 або вище."), + ("android_version_audio_tip", "Поточна версія Android не підтримує захоплення звуку, будь ласка, оновіться до Android 10 або вище."), ("android_start_service_tip", "Натисніть [Запустити службу] або увімкніть дозвіл на [Захоплення екрана], щоб запустити службу спільного доступу до екрана."), - ("android_permission_may_not_change_tip", "Дозволи для встановлених зʼєднань можуть не змінитися миттєво аж до перепідключення."), + ("android_permission_may_not_change_tip", "Дозволи для встановлених зʼєднань можуть не застосуватися аж до перепідключення."), ("Account", "Обліковий запис"), ("Overwrite", "Перезаписати"), ("This file exists, skip or overwrite this file?", "Цей файл існує, пропустити чи перезаписати файл?"), @@ -373,7 +373,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Write a message", "Написати повідомлення"), ("Prompt", "Підказка"), ("Please wait for confirmation of UAC...", "Будь ласка, зачекайте підтвердження UAC..."), - ("elevated_foreground_window_tip", "Поточне вікно віддаленої стільниці потребує розширених прав для роботи, тому наразі неможливо використати мишу та клавіатуру. Ви можете запропонувати віддаленому користувачу згорнути поточне вікно чи натиснути кнопку розширення прав у вікні керування підключеннями. Для уникнення цієї проблеми, рекомендується встановити програму на віддаленому пристрої"), + ("elevated_foreground_window_tip", "Поточне вікно віддаленої стільниці потребує розширених прав для роботи, тому наразі неможливо використовувати мишу та клавіатуру. Ви можете запропонувати віддаленому користувачеві згорнути поточне вікно чи натиснути кнопку розширення прав у вікні керування підключеннями. Для уникнення цієї проблеми, рекомендується встановити програму на віддаленому пристрої."), ("Disconnected", "Відʼєднано"), ("Other", "Інше"), ("Confirm before closing multiple tabs", "Підтверджувати перед закриттям кількох вкладок"), @@ -413,7 +413,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always use software rendering", "Завжди використовувати програмну візуалізацію"), ("config_input", "Для віддаленого керування віддаленою стільницею з клавіатури, вам необхідно надати RustDesk дозволи на \"Відстеження введення\""), ("config_microphone", "Для можливості віддаленої розмови, вам необхідно надати RustDesk дозвіл на \"Запис аудіо\""), - ("request_elevation_tip", "Ви також можете надіслати запит на розширення прав, в разі присутності особи з віддаленого боку."), + ("request_elevation_tip", "Ви можете також надіслати запит на розширення прав, в разі присутності особи з віддаленого боку."), ("Wait", "Зачекайте"), ("Elevation Error", "Невдала спроба розширення прав"), ("Ask the remote user for authentication", "Попросіть віддаленого користувача пройти автентифікацію"), @@ -460,8 +460,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Your Device", "Вам пристрій"), ("empty_recent_tip", "Овва, відсутні нещодавні сеанси!\nСаме час запланувати нове підключення."), ("empty_favorite_tip", "Досі немає улюблених вузлів?\nДавайте організуємо нове підключення та додамо його до улюблених!"), - ("empty_lan_tip", "О ні, схоже ми поки не виявили жодного віддаленого пристрою"), - ("empty_address_book_tip", "Ой лишенько, схоже до вашої адресної книги немає жодного віддаленого пристрою"), + ("empty_lan_tip", "О ні, схоже ми ще не виявили жодного віддаленого пристрою."), + ("empty_address_book_tip", "Ой лишенько, схоже у вашій адресній книзі немає жодного віддаленого пристрою."), ("eg: admin", "напр., admin"), ("Empty Username", "Незаповнене імʼя"), ("Empty Password", "Незаповнений пароль"), @@ -526,7 +526,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Toggle Tags", "Видимість тегів"), ("pull_ab_failed_tip", "Не вдалося оновити адресну книгу"), ("push_ab_failed_tip", "Не вдалося синхронізувати адресну книгу"), - ("synced_peer_readded_tip", "Пристрої з нещодавніх сеансів будуть синхронізовані з адресною книгою"), + ("synced_peer_readded_tip", "Пристрої з нещодавніх сеансів будуть синхронізовані з адресною книгою."), ("Change Color", "Змінити колір"), ("Primary Color", "Основний колір"), ("HSV Color", "Колір HSV"), @@ -544,7 +544,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("auto_disconnect_option_tip", "Автоматично завершувати вхідні сеанси в разі неактивності користувача"), ("Connection failed due to inactivity", "Зʼєднання розірвано через неактивність"), ("Check for software update on startup", "Перевіряти оновлення під час запуску"), - ("upgrade_rustdesk_server_pro_to_{}_tip", "Будь ласка, оновіть RustDesk Server Pro до версії {} чи більш актуальної!"), + ("upgrade_rustdesk_server_pro_to_{}_tip", "Будь ласка, оновіть RustDesk Server Pro до версії {} чи новіше!"), ("pull_group_failed_tip", "Не вдалося оновити групу"), ("Filter by intersection", "Фільтр за збігом"), ("Remove wallpaper during incoming sessions", "Прибирати шпалеру під час вхідних сеансів"), @@ -619,18 +619,18 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("During controlled", "Коли керується"), ("During service is on", "Коли запущена служба"), ("Capture screen using DirectX", "Захоплення екрана з використанням DirectX"), - ("Back", ""), - ("Apps", ""), - ("Volume up", ""), - ("Volume down", ""), - ("Power", ""), - ("Telegram bot", ""), - ("enable-bot-tip", ""), - ("enable-bot-desc", ""), - ("cancel-2fa-confirm-tip", ""), - ("cancel-bot-confirm-tip", ""), - ("About RustDesk", ""), - ("Send clipboard keystrokes", ""), - ("network_error_tip", ""), + ("Back", "Назад"), + ("Apps", "Застосунки"), + ("Volume up", "Збільшити гучність"), + ("Volume down", "Зменшити гучність"), + ("Power", "Живлення"), + ("Telegram bot", "Бот Telegram"), + ("enable-bot-tip", "Надає можливість отримувати код двофакторної автентифікації від вашого бота. Також може сповіщати про підключення"), + ("enable-bot-desc", "1. Відкрийте чат з @BotFather.\n2. Надішліть команду \"/newbot\". Ви отримаєте токен.\n3. Почніть чат з вашим щойно створеним ботом. Щоб активувати його, надішліть повідомлення, що починається зі скісної риски (\"/\"), наприклад \"/hello\".\n"), + ("cancel-2fa-confirm-tip", "Ви впевнені, що хочете скасувати код двофакторної автентифікації?"), + ("cancel-bot-confirm-tip", "Ви впевнені, що хочете скасувати Telegram бота?"), + ("About RustDesk", "Про Rustdesk"), + ("Send clipboard keystrokes", "Надіслати вміст буфера обміну"), + ("network_error_tip", "Будь ласка, перевірте ваше підключення до мережі та натисність \"Повторити\""), ].iter().cloned().collect(); } From b6035fbbdfffb9d6a083e8754e786a8abaa3feaf Mon Sep 17 00:00:00 2001 From: 21pages Date: Fri, 2 Aug 2024 15:12:48 +0800 Subject: [PATCH 040/541] update supportedLocales (#8925) Signed-off-by: 21pages --- flutter/lib/common.dart | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 2f526fb483e6..45c78f3d92d8 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -629,10 +629,30 @@ List supportedLocales = const [ Locale('da'), Locale('eo'), Locale('tr'), - Locale('vi'), - Locale('pl'), Locale('kz'), Locale('es'), + Locale('nl'), + Locale('nb'), + Locale('et'), + Locale('eu'), + Locale('bg'), + Locale('be'), + Locale('vn'), + Locale('uk'), + Locale('fa'), + Locale('ca'), + Locale('el'), + Locale('sv'), + Locale('sq'), + Locale('sr'), + Locale('th'), + Locale('sl'), + Locale('ro'), + Locale('lt'), + Locale('lv'), + Locale('ar'), + Locale('he'), + Locale('hr'), ]; String formatDurationToTime(Duration duration) { From d9fba50606ef19065b7cff4bdd40dfdb9a3ca706 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Fri, 2 Aug 2024 15:43:55 +0800 Subject: [PATCH 041/541] fix https://github.com/rustdesk/rustdesk-server-pro/issues/334 again --- src/client.rs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/client.rs b/src/client.rs index d6d8f8546162..4156de133c6d 100644 --- a/src/client.rs +++ b/src/client.rs @@ -2111,11 +2111,15 @@ impl LoginConfigHandler { }; let mut display_name = get_buildin_option(config::keys::OPTION_DISPLAY_NAME); if display_name.is_empty() { - display_name = serde_json::from_str::>( - &LocalConfig::get_option("user_info"), - ) - .map(|mut x| x.remove("name").unwrap_or_default()) - .unwrap_or_default(); + display_name = + serde_json::from_str::(&LocalConfig::get_option("user_info")) + .map(|x| { + x.get("name") + .map(|x| x.as_str().unwrap_or_default()) + .unwrap_or_default() + .to_owned() + }) + .unwrap_or_default(); } if display_name.is_empty() { display_name = crate::username(); From 5e7d4fd2d62fb5fb45b18453f2c195a1316a2213 Mon Sep 17 00:00:00 2001 From: Vasyl Gello Date: Fri, 2 Aug 2024 10:43:30 +0000 Subject: [PATCH 042/541] vcpkg deps (#8926) * vcpkg: bump opus to 1.5.2 Should fix flakes caused by https://github.com/android/ndk/issues/2032 Signed-off-by: Vasyl Gello * vcpkg: actually use cached artifacts Signed-off-by: Vasyl Gello * Print all vcpkg log files on errors Signed-off-by: Vasyl Gello --------- Signed-off-by: Vasyl Gello --- .github/workflows/flutter-build.yml | 104 +++++++++++++++++++++++++--- res/vcpkg/opus/portfile.cmake | 2 +- res/vcpkg/opus/vcpkg.json | 2 +- 3 files changed, 96 insertions(+), 12 deletions(-) diff --git a/.github/workflows/flutter-build.yml b/.github/workflows/flutter-build.yml index 6ca9d13609d3..530a3186c4c2 100644 --- a/.github/workflows/flutter-build.yml +++ b/.github/workflows/flutter-build.yml @@ -120,12 +120,25 @@ jobs: with: vcpkgDirectory: C:\vcpkg vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }} + doNotCache: false - name: Install vcpkg dependencies env: VCPKG_DEFAULT_HOST_TRIPLET: ${{ matrix.job.vcpkg-triplet }} run: | - $VCPKG_ROOT/vcpkg install --triplet ${{ matrix.job.vcpkg-triplet }} --x-install-root="$VCPKG_ROOT/installed" + if ! $VCPKG_ROOT/vcpkg \ + install \ + --triplet ${{ matrix.job.vcpkg-triplet }} \ + --x-install-root="$VCPKG_ROOT/installed"; then + find "${VCPKG_ROOT}/" -name "*.log" | while read -r _1; do + echo "$_1:" + echo "======" + cat "$_1" + echo "======" + echo "" + done + exit 1 + fi shell: bash - name: Build rustdesk @@ -267,12 +280,25 @@ jobs: with: vcpkgDirectory: C:\vcpkg vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }} + doNotCache: false - name: Install vcpkg dependencies env: VCPKG_DEFAULT_HOST_TRIPLET: ${{ matrix.job.vcpkg-triplet }} run: | - $VCPKG_ROOT/vcpkg install --triplet ${{ matrix.job.vcpkg-triplet }} --x-install-root="$VCPKG_ROOT/installed" + if ! $VCPKG_ROOT/vcpkg \ + install \ + --triplet ${{ matrix.job.vcpkg-triplet }} \ + --x-install-root="$VCPKG_ROOT/installed"; then + find "${VCPKG_ROOT}/" -name "*.log" | while read -r _1; do + echo "$_1:" + echo "======" + cat "$_1" + echo "======" + echo "" + done + exit 1 + fi shell: bash - name: Build rustdesk @@ -453,10 +479,23 @@ jobs: uses: lukka/run-vcpkg@v11 with: vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }} + doNotCache: false - name: Install vcpkg dependencies run: | - $VCPKG_ROOT/vcpkg install --triplet ${{ matrix.job.vcpkg-triplet }} --x-install-root="$VCPKG_ROOT/installed" + if ! $VCPKG_ROOT/vcpkg \ + install \ + --triplet ${{ matrix.job.vcpkg-triplet }} \ + --x-install-root="$VCPKG_ROOT/installed"; then + find "${VCPKG_ROOT}/" -name "*.log" | while read -r _1; do + echo "$_1:" + echo "======" + cat "$_1" + echo "======" + echo "" + done + exit 1 + fi shell: bash - name: Install Rust toolchain @@ -667,10 +706,22 @@ jobs: uses: lukka/run-vcpkg@v11 with: vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }} + doNotCache: false - name: Install vcpkg dependencies run: | - $VCPKG_ROOT/vcpkg install --x-install-root="$VCPKG_ROOT/installed" + if ! $VCPKG_ROOT/vcpkg \ + install \ + --x-install-root="$VCPKG_ROOT/installed"; then + find "${VCPKG_ROOT}/" -name "*.log" | while read -r _1; do + echo "$_1:" + echo "======" + cat "$_1" + echo "======" + echo "" + done + exit 1 + fi - name: Show version information (Rust, cargo, Clang) shell: bash @@ -864,23 +915,34 @@ jobs: with: vcpkgDirectory: /opt/artifacts/vcpkg vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }} + doNotCache: false - name: Install vcpkg dependencies run: | case ${{ matrix.job.target }} in aarch64-linux-android) - ./flutter/build_android_deps.sh arm64-v8a + ANDROID_TARGET=arm64-v8a ;; armv7-linux-androideabi) - ./flutter/build_android_deps.sh armeabi-v7a + ANDROID_TARGET=armeabi-v7a ;; x86_64-linux-android) - ./flutter/build_android_deps.sh x86_64 + ANDROID_TARGET=x86_64 ;; i686-linux-android) - ./flutter/build_android_deps.sh x86 + ANDROID_TARGET=x86 ;; esac + if ! ./flutter/build_android_deps.sh "${ANDROID_TARGET}"; then + find "${VCPKG_ROOT}/" -name "*.log" | while read -r _1; do + echo "$_1:" + echo "======" + cat "$_1" + echo "======" + echo "" + done + exit 1 + fi shell: bash - name: Restore bridge files @@ -1113,11 +1175,24 @@ jobs: with: vcpkgDirectory: /opt/artifacts/vcpkg vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }} + doNotCache: false - name: Install vcpkg dependencies if: matrix.job.arch == 'x86_64' || env.UPLOAD_ARTIFACT == 'true' run: | - $VCPKG_ROOT/vcpkg install --triplet ${{ matrix.job.vcpkg-triplet }} --x-install-root="$VCPKG_ROOT/installed" + if ! $VCPKG_ROOT/vcpkg \ + install \ + --triplet ${{ matrix.job.vcpkg-triplet }} \ + --x-install-root="$VCPKG_ROOT/installed"; then + find "${VCPKG_ROOT}/" -name "*.log" | while read -r _1; do + echo "$_1:" + echo "======" + cat "$_1" + echo "======" + echo "" + done + exit 1 + fi shell: bash - name: Restore bridge files @@ -1500,7 +1575,16 @@ jobs: cat ~/.cargo/config # install dependencies from vcpkg export VCPKG_ROOT=/opt/artifacts/vcpkg - $VCPKG_ROOT/vcpkg install --triplet ${{ matrix.job.vcpkg-triplet }} --x-install-root="$VCPKG_ROOT/installed" + if ! $VCPKG_ROOT/vcpkg install --triplet ${{ matrix.job.vcpkg-triplet }} --x-install-root="$VCPKG_ROOT/installed"; then + find "${VCPKG_ROOT}/" -name "*.log" | while read -r _1; do + echo "$_1:" + echo "======" + cat "$_1" + echo "======" + echo "" + done + exit 1 + fi # build rustdesk python3 ./res/inline-sciter.py export CARGO_INCREMENTAL=0 diff --git a/res/vcpkg/opus/portfile.cmake b/res/vcpkg/opus/portfile.cmake index 3e37dc1d8c22..b4288b2200cc 100644 --- a/res/vcpkg/opus/portfile.cmake +++ b/res/vcpkg/opus/portfile.cmake @@ -2,7 +2,7 @@ vcpkg_from_github( OUT_SOURCE_PATH SOURCE_PATH REPO xiph/opus REF "v${VERSION}" - SHA512 ba79ad035993e7bc4c09b7d77964ba913eb0b2be33305e8a04a8c49aaab21c4d96ac828e31ae45484896105851fdfc8c305c63c8400e4481dd76c62a1c12286b + SHA512 4ffefd9c035671024f9720c5129bfe395dea04f0d6b730041c2804e89b1db6e4d19633ad1ae58855afc355034233537361e707f26dc53adac916554830038fab HEAD_REF main PATCHES fix-pkgconfig-version.patch ) diff --git a/res/vcpkg/opus/vcpkg.json b/res/vcpkg/opus/vcpkg.json index 4edeaa1e042c..574879eed651 100644 --- a/res/vcpkg/opus/vcpkg.json +++ b/res/vcpkg/opus/vcpkg.json @@ -1,6 +1,6 @@ { "name": "opus", - "version": "1.5.1", + "version": "1.5.2", "description": "Totally open, royalty-free, highly versatile audio codec", "homepage": "https://github.com/xiph/opus", "license": "BSD-3-Clause", From 6eea4252805a989a3743d954a6a28c64932c408b Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Fri, 2 Aug 2024 21:36:49 +0800 Subject: [PATCH 043/541] fix: clipboard cm ipc data, raw bytes (#8930) * fix: clipboard cm ipc data, raw bytes Signed-off-by: fufesou * Remove useless check Signed-off-by: fufesou --------- Signed-off-by: fufesou --- src/server/clipboard_service.rs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/server/clipboard_service.rs b/src/server/clipboard_service.rs index d07fc74b43b4..3040a8f88f33 100644 --- a/src/server/clipboard_service.rs +++ b/src/server/clipboard_service.rs @@ -150,14 +150,8 @@ impl Handler { Ok(Ok(mut data)) => { for c in &mut contents { if c.next_raw { - if c.content_len <= data.len() { - c.content = - data.split_off(c.content_len).into(); - } else { - // Reconnect the next time to avoid the next raw data mismatch. - self.stream = None; - bail!("failed to get raw clipboard data: invalid size"); - } + // No need to check the length because sum(content_len) == data.len(). + c.content = data.split_to(c.content_len).into(); } } } From 2dd3d8c11e5d2a02cc1805718f7d0d965b375c66 Mon Sep 17 00:00:00 2001 From: Yevhen Popok Date: Fri, 2 Aug 2024 17:58:45 +0300 Subject: [PATCH 044/541] Use correct locale code for Ukrainian translation file name (#8932) Rename Ukrainian translation file ua.rs to uk.rs to avoid confusion. This complements commits [2333ee2](https://github.com/rustdesk/rustdesk/commit/2333ee2c07c6a67ed6c58ccfbc50a3bee203f90d) and [b6035fb](https://github.com/21pages/rustdesk/commit/b6035fbbdfffb9d6a083e8754e786a8abaa3feaf) --- src/lang.rs | 4 ++-- src/lang/{ua.rs => uk.rs} | 0 2 files changed, 2 insertions(+), 2 deletions(-) rename src/lang/{ua.rs => uk.rs} (100%) diff --git a/src/lang.rs b/src/lang.rs index d66b3895661e..706822678037 100644 --- a/src/lang.rs +++ b/src/lang.rs @@ -41,7 +41,7 @@ mod sv; mod th; mod tr; mod tw; -mod ua; +mod uk; mod vn; pub const LANGS: &[(&str, &str)] = &[ @@ -144,7 +144,7 @@ pub fn translate_locale(name: String, locale: &str) -> String { "ja" => ja::T.deref(), "ko" => ko::T.deref(), "kz" => kz::T.deref(), - "uk" => ua::T.deref(), + "uk" => uk::T.deref(), "fa" => fa::T.deref(), "ca" => ca::T.deref(), "el" => el::T.deref(), diff --git a/src/lang/ua.rs b/src/lang/uk.rs similarity index 100% rename from src/lang/ua.rs rename to src/lang/uk.rs From f899b2a962e169a0e58e6b98c84d1fe9035f58de Mon Sep 17 00:00:00 2001 From: rustdesk Date: Sat, 3 Aug 2024 18:04:52 +0800 Subject: [PATCH 045/541] fix https://github.com/rustdesk/rustdesk/issues/8856 --- Cargo.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index ca6bffe1f992..8f40d997af70 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5778,7 +5778,7 @@ dependencies = [ [[package]] name = "sciter-rs" version = "0.5.57" -source = "git+https://github.com/open-trade/rust-sciter?branch=dyn#fab913b7c2e779b05c249b0c5de5a08759b2c15d" +source = "git+https://github.com/open-trade/rust-sciter?branch=dyn#5322f3a755a0e6bf999fbc60d1efc35246c0f821" dependencies = [ "lazy_static", "libc", From ba43424781ccc5c380906742c13ca860b5c0e849 Mon Sep 17 00:00:00 2001 From: Vasyl Gello Date: Sat, 3 Aug 2024 10:39:12 +0000 Subject: [PATCH 046/541] Build universal apk (#8941) Signed-off-by: Vasyl Gello --- .github/workflows/flutter-build.yml | 150 ++++++++++++++++++++++++++++ 1 file changed, 150 insertions(+) diff --git a/.github/workflows/flutter-build.yml b/.github/workflows/flutter-build.yml index 530a3186c4c2..4ac2267eb361 100644 --- a/.github/workflows/flutter-build.yml +++ b/.github/workflows/flutter-build.yml @@ -999,6 +999,12 @@ jobs: ;; esac + - name: Upload Rustdesk library to Artifacts + uses: actions/upload-artifact@master + with: + name: librustdesk.so.${{ matrix.job.target }} + path: ./target/${{ matrix.job.target }}/release/liblibrustdesk.so + - name: Build rustdesk shell: bash env: @@ -1088,6 +1094,150 @@ jobs: files: | signed-apk/rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.apk + build-rustdesk-android-universal: + needs: [build-rustdesk-android] + name: build rustdesk android universal apk + runs-on: ubuntu-20.04 + env: + reltype: release + 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: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y \ + clang \ + cmake \ + curl \ + gcc-multilib \ + git \ + g++ \ + g++-multilib \ + libappindicator3-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 \ + ninja-build \ + openjdk-11-jdk-headless \ + pkg-config \ + tree \ + wget + + - name: Checkout source code + uses: actions/checkout@v4 + - name: Install flutter + uses: subosito/flutter-action@v2 + with: + channel: "stable" + flutter-version: ${{ env.ANDROID_FLUTTER_VERSION }} + + - name: Restore bridge files + uses: actions/download-artifact@master + with: + name: bridge-artifact + path: ./ + + - name: Download Rustdesk library from Artifacts + uses: actions/download-artifact@master + with: + name: librustdesk.so.aarch64-linux-android + path: ./flutter/android/app/src/main/jniLibs/arm64-v8a/librustdesk.so + + - name: Download Rustdesk library from Artifacts + uses: actions/download-artifact@master + with: + name: librustdesk.so.armv7-linux-androideabi + path: ./flutter/android/app/src/main/jniLibs/armeabi-v7a/librustdesk.so + + - name: Download Rustdesk library from Artifacts + uses: actions/download-artifact@master + with: + name: librustdesk.so.x86_64-linux-android + path: ./flutter/android/app/src/main/jniLibs/x86_64/librustdesk.so + + - name: fix android for flutter 3.13 + if: $${{ env.ANDROID_FLUTTER_VERSION == '3.13.9' }} + run: | + sed -i 's/uni_links_desktop/#uni_links_desktop/g' flutter/pubspec.yaml + cd flutter/lib + find . | grep dart | xargs sed -i 's/textScaler: TextScaler.linear(\(.*\)),/textScaleFactor: \1,/g' + + - 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 + 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 ${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 ${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/x86_64-linux-android/libc++_shared.so ./flutter/android/app/src/main/jniLibs/x86_64/ + # build flutter + pushd flutter + flutter build apk "--${{ env.reltype }}" --target-platform android-arm64,android-arm,android-x64 + mkdir -p signed-apk + mv build/app/outputs/flutter-apk/app-${{ matrix.job.reltype }}.apk ../signed-apk/rustdesk-${{ env.VERSION }}-universal.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: Upload Artifacts + if: env.ANDROID_SIGNING_KEY != null && env.UPLOAD_ARTIFACT == 'true' + uses: actions/upload-artifact@master + with: + name: rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.apk + path: ${{steps.sign-rustdesk.outputs.signedReleaseFile}} + + - name: Publish signed apk package + if: env.ANDROID_SIGNING_KEY != null && env.UPLOAD_ARTIFACT == 'true' + uses: softprops/action-gh-release@v1 + with: + prerelease: true + tag_name: ${{ env.TAG_NAME }} + files: | + ${{steps.sign-rustdesk.outputs.signedReleaseFile}} + + - name: Publish unsigned apk package + if: env.ANDROID_SIGNING_KEY == null && env.UPLOAD_ARTIFACT == 'true' + uses: softprops/action-gh-release@v1 + with: + prerelease: true + tag_name: ${{ env.TAG_NAME }} + files: | + signed-apk/rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.apk + build-rustdesk-linux: needs: [generate-bridge-linux] name: build rustdesk linux ${{ matrix.job.target }} From 31a1b7a80b42bc5ecf76b48cef172b9699ada703 Mon Sep 17 00:00:00 2001 From: Vasyl Gello Date: Sat, 3 Aug 2024 11:00:27 +0000 Subject: [PATCH 047/541] Fix copy step for universal apk (#8942) Signed-off-by: Vasyl Gello --- .github/workflows/flutter-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/flutter-build.yml b/.github/workflows/flutter-build.yml index 4ac2267eb361..d26cb663f613 100644 --- a/.github/workflows/flutter-build.yml +++ b/.github/workflows/flutter-build.yml @@ -1197,7 +1197,7 @@ jobs: pushd flutter flutter build apk "--${{ env.reltype }}" --target-platform android-arm64,android-arm,android-x64 mkdir -p signed-apk - mv build/app/outputs/flutter-apk/app-${{ matrix.job.reltype }}.apk ../signed-apk/rustdesk-${{ env.VERSION }}-universal.apk + mv build/app/outputs/flutter-apk/app-${{ env.reltype }}.apk ../signed-apk/rustdesk-${{ env.VERSION }}-universal.apk - uses: r0adkll/sign-android-release@v1 name: Sign app APK From 508dd5b38368223fb34e14bc9dc350087bdf54be Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Sat, 3 Aug 2024 22:29:30 +0800 Subject: [PATCH 048/541] fix: custom client, msi, conn type (#8944) Signed-off-by: fufesou --- res/msi/preprocess.py | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/res/msi/preprocess.py b/res/msi/preprocess.py index 9a6a3bcab874..a5251c1cef62 100644 --- a/res/msi/preprocess.py +++ b/res/msi/preprocess.py @@ -65,10 +65,10 @@ def make_parser(): "-c", "--custom", action="store_true", help="Is custom client", default=False ) parser.add_argument( - "--custom-client-props", + "--conn-type", type=str, - default="{}", - help='Custom client properties, e.g. \'{"connection-type": "outgoing"}\'', + default="", + help='Connection type, e.g. "incoming", "outgoing". Default is empty, means incoming-outgoing', ) parser.add_argument( "--app-name", type=str, default="RustDesk", help="The app name." @@ -391,21 +391,14 @@ def gen_custom_ARPSYSTEMCOMPONENT(args, dist_dir): else: return gen_custom_ARPSYSTEMCOMPONENT_False(args) -def gen_custom_client_properties(args): - try: - props = json.loads(args.custom_client_props) - except json.JSONDecodeError as e: - print(f"Failed to decode custom props: {e}") - return False - +def gen_conn_type(args): def func(lines, index_start): indent = g_indent_unit * 3 lines_new = [] - - if 'connection-type' in props: + if args.conn_type != "": lines_new.append( - f"""{indent}\n""" + f"""{indent}\n""" ) for i, line in enumerate(lines_new): @@ -539,7 +532,7 @@ def replace_component_guids_in_wxs(): if not gen_custom_ARPSYSTEMCOMPONENT(args, dist_dir): sys.exit(-1) - if not gen_custom_client_properties(args): + if not gen_conn_type(args): sys.exit(-1) if not gen_auto_component(app_name, dist_dir): From 0d1d7a9b8781e81c4f9ea3b40a7a466d25da4226 Mon Sep 17 00:00:00 2001 From: Vasyl Gello Date: Sun, 4 Aug 2024 04:34:08 +0000 Subject: [PATCH 049/541] Guard parameters for universal apk (#8943) * Guard parameters for universal apk Signed-off-by: Vasyl Gello * Free space before android builds Signed-off-by: Vasyl Gello --------- Signed-off-by: Vasyl Gello --- .github/workflows/flutter-build.yml | 51 +++++++++++++++++++++++++---- 1 file changed, 45 insertions(+), 6 deletions(-) diff --git a/.github/workflows/flutter-build.yml b/.github/workflows/flutter-build.yml index d26cb663f613..1493e99832a8 100644 --- a/.github/workflows/flutter-build.yml +++ b/.github/workflows/flutter-build.yml @@ -855,6 +855,17 @@ jobs: suffix: "", } steps: + - name: Free Disk Space (Ubuntu) + uses: jlumbroso/free-disk-space@main + with: + tool-cache: false + android: false + dotnet: true + haskell: true + large-packages: false + docker-images: true + swap-storage: false + - name: Export GitHub Actions cache environment variables uses: actions/github-script@v6 with: @@ -1100,7 +1111,20 @@ jobs: runs-on: ubuntu-20.04 env: reltype: release + x86_target: "" # can be ",android-x86" + suffix: "" steps: + - name: Free Disk Space (Ubuntu) + uses: jlumbroso/free-disk-space@main + with: + tool-cache: false + android: false + dotnet: true + haskell: true + large-packages: false + docker-images: true + swap-storage: false + - name: Export GitHub Actions cache environment variables uses: actions/github-script@v6 with: @@ -1161,19 +1185,26 @@ jobs: uses: actions/download-artifact@master with: name: librustdesk.so.aarch64-linux-android - path: ./flutter/android/app/src/main/jniLibs/arm64-v8a/librustdesk.so + path: ./flutter/android/app/src/main/jniLibs/arm64-v8a - name: Download Rustdesk library from Artifacts uses: actions/download-artifact@master with: name: librustdesk.so.armv7-linux-androideabi - path: ./flutter/android/app/src/main/jniLibs/armeabi-v7a/librustdesk.so + path: ./flutter/android/app/src/main/jniLibs/armeabi-v7a - name: Download Rustdesk library from Artifacts uses: actions/download-artifact@master with: name: librustdesk.so.x86_64-linux-android - path: ./flutter/android/app/src/main/jniLibs/x86_64/librustdesk.so + path: ./flutter/android/app/src/main/jniLibs/x86_64 + + - name: Download Rustdesk library from Artifacts + if: ${{ env.reltype == 'debug' }} + uses: actions/download-artifact@master + with: + name: librustdesk.so.i686-linux-android + path: ./flutter/android/app/src/main/jniLibs/x86 - name: fix android for flutter 3.13 if: $${{ env.ANDROID_FLUTTER_VERSION == '3.13.9' }} @@ -1190,14 +1221,22 @@ jobs: 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 + mv ./flutter/android/app/src/main/jniLibs/arm64-v8a/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/arm64-v8a/librustdesk.so 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/ + mv ./flutter/android/app/src/main/jniLibs/armeabi-v7a/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/armeabi-v7a/librustdesk.so 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/ + mv ./flutter/android/app/src/main/jniLibs/x86_64/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/x86_64/librustdesk.so cp ${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/x86_64-linux-android/libc++_shared.so ./flutter/android/app/src/main/jniLibs/x86_64/ + if [ "${{ env.reltype }}" = "debug" ]; then + mv ./flutter/android/app/src/main/jniLibs/x86/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/x86/librustdesk.so + cp ${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/i686-linux-android/libc++_shared.so ./flutter/android/app/src/main/jniLibs/x86/ + fi # build flutter pushd flutter - flutter build apk "--${{ env.reltype }}" --target-platform android-arm64,android-arm,android-x64 + flutter build apk "--${{ env.reltype }}" --target-platform android-arm64,android-arm,android-x64${{ env.x86_target }} + popd mkdir -p signed-apk - mv build/app/outputs/flutter-apk/app-${{ env.reltype }}.apk ../signed-apk/rustdesk-${{ env.VERSION }}-universal.apk + mv ./flutter/build/app/outputs/flutter-apk/app-${{ env.reltype }}.apk signed-apk/rustdesk-${{ env.VERSION }}-universal${{ env.suffix }}.apk - uses: r0adkll/sign-android-release@v1 name: Sign app APK @@ -1236,7 +1275,7 @@ jobs: prerelease: true tag_name: ${{ env.TAG_NAME }} files: | - signed-apk/rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.apk + signed-apk/rustdesk-${{ env.VERSION }}-universal${{ env.suffix }}.apk build-rustdesk-linux: needs: [generate-bridge-linux] From b6ba9978e37b2caec34b4b4ea1af10fb4fef5bef Mon Sep 17 00:00:00 2001 From: 21pages Date: Sun, 4 Aug 2024 16:11:00 +0800 Subject: [PATCH 050/541] set max audio buffer to 150ms, clear audio buffer if full (#8947) The device should have the capability to play a sufficient audio buffer during each period to meet the audio config, so the playback speed is not slow. The audio delay is caused by network jitter. The controlled side sends audio data every 10ms, but it often happens that multiple packets are sent together. During underrun periods, the controlling side plays extra silence data instead, resulting in the device playing more audio than the configured amount. --- src/client.rs | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/src/client.rs b/src/client.rs index 4156de133c6d..1f2501506c88 100644 --- a/src/client.rs +++ b/src/client.rs @@ -113,6 +113,9 @@ pub const SCRAP_OTHER_VERSION_OR_X11_REQUIRED: &str = pub const SCRAP_X11_REQUIRED: &str = "x11 expected"; pub const SCRAP_X11_REF_URL: &str = "https://rustdesk.com/docs/en/manual/linux/#x11-required"; +#[cfg(not(any(target_os = "android", target_os = "linux")))] +pub const AUDIO_BUFFER_MS: usize = 150; + #[cfg(feature = "flutter")] #[cfg(not(any(target_os = "android", target_os = "ios")))] pub(crate) struct ClientClipboardContext; @@ -903,11 +906,33 @@ struct AudioBuffer(pub Arc>>); impl Default for AudioBuffer { fn default() -> Self { Self(Arc::new(std::sync::Mutex::new( - ringbuf::HeapRb::::new(48000 * 2), // 48000hz, 2 channel, 1 second + ringbuf::HeapRb::::new(48000 * 2 * AUDIO_BUFFER_MS / 1000), // 48000hz, 2 channel ))) } } +#[cfg(not(any(target_os = "android", target_os = "linux")))] +impl AudioBuffer { + pub fn resize(&self, sample_rate: usize, channels: usize) { + let capacity = sample_rate * channels * AUDIO_BUFFER_MS / 1000; + let old_capacity = self.0.lock().unwrap().capacity(); + if capacity != old_capacity { + *self.0.lock().unwrap() = ringbuf::HeapRb::::new(capacity); + log::info!("Audio buffer resized from {old_capacity} to {capacity}"); + } + } + + // clear when full to avoid long time noise + #[inline] + pub fn clear_if_full(&self) { + let full = self.0.lock().unwrap().is_full(); + if full { + self.0.lock().unwrap().clear(); + log::info!("Audio buffer cleared"); + } + } +} + impl AudioHandler { /// Start the audio playback. #[cfg(target_os = "linux")] @@ -1052,6 +1077,7 @@ impl AudioHandler { self.device_channel, ); } + self.audio_buffer.clear_if_full(); audio_buffer.lock().unwrap().push_slice_overwrite(&buffer); } #[cfg(target_os = "android")] @@ -1080,6 +1106,8 @@ impl AudioHandler { // too many errors, will improve later log::trace!("an error occurred on stream: {}", err); }; + self.audio_buffer + .resize(config.sample_rate.0 as _, config.channels as _); let audio_buffer = self.audio_buffer.0.clone(); let ready = self.ready.clone(); let timeout = None; From 9b8209b61bb66623edae7eee93175d913b94be6e Mon Sep 17 00:00:00 2001 From: rustdesk Date: Sun, 4 Aug 2024 16:17:59 +0800 Subject: [PATCH 051/541] log::trace audio buffer cleared --- src/client.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client.rs b/src/client.rs index 1f2501506c88..48e8033b9ad8 100644 --- a/src/client.rs +++ b/src/client.rs @@ -928,7 +928,7 @@ impl AudioBuffer { let full = self.0.lock().unwrap().is_full(); if full { self.0.lock().unwrap().clear(); - log::info!("Audio buffer cleared"); + log::trace!("Audio buffer cleared"); } } } From cb0dc46d08927513ff024e2f6546a09672874bbc Mon Sep 17 00:00:00 2001 From: rustdesk Date: Sun, 4 Aug 2024 16:22:42 +0800 Subject: [PATCH 052/541] turn it off since it does not work, please test it before submit PR --- .github/workflows/flutter-build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/flutter-build.yml b/.github/workflows/flutter-build.yml index 1493e99832a8..a5922057781b 100644 --- a/.github/workflows/flutter-build.yml +++ b/.github/workflows/flutter-build.yml @@ -1106,6 +1106,7 @@ jobs: signed-apk/rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.apk build-rustdesk-android-universal: + if: false needs: [build-rustdesk-android] name: build rustdesk android universal apk runs-on: ubuntu-20.04 From 7bf5e69444c42f6fba46a5ef062499beb5e3cd10 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Sun, 4 Aug 2024 16:23:58 +0800 Subject: [PATCH 053/541] revert --- .github/workflows/flutter-build.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/flutter-build.yml b/.github/workflows/flutter-build.yml index a5922057781b..1493e99832a8 100644 --- a/.github/workflows/flutter-build.yml +++ b/.github/workflows/flutter-build.yml @@ -1106,7 +1106,6 @@ jobs: signed-apk/rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.apk build-rustdesk-android-universal: - if: false needs: [build-rustdesk-android] name: build rustdesk android universal apk runs-on: ubuntu-20.04 From eafebdba210cad50c837fbf6607df6e22e494ee8 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Sun, 4 Aug 2024 16:32:05 +0800 Subject: [PATCH 054/541] run universal for upload only --- .github/workflows/flutter-build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/flutter-build.yml b/.github/workflows/flutter-build.yml index 1493e99832a8..74ebe72a4b63 100644 --- a/.github/workflows/flutter-build.yml +++ b/.github/workflows/flutter-build.yml @@ -1108,6 +1108,7 @@ jobs: build-rustdesk-android-universal: needs: [build-rustdesk-android] name: build rustdesk android universal apk + if: ${{ inputs.upload-artifact }} runs-on: ubuntu-20.04 env: reltype: release From e58e75eea9df83b866726cb3ea0f32d3a1a124a1 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Sun, 4 Aug 2024 18:45:49 +0800 Subject: [PATCH 055/541] add address_book_name, address_book_tag to cli, https://github.com/rustdesk/rustdesk/discussions/7866, need to use with server Pro 1.4.2 (not ready) --- src/core_main.rs | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/src/core_main.rs b/src/core_main.rs index bf7e536f6b27..942066ae3584 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -377,13 +377,31 @@ pub fn core_main() -> Option> { if pos < max { strategy_name = Some(args[pos + 1].to_owned()); } + let mut address_book_name = None; + let pos = args + .iter() + .position(|x| x == "--address_book_name") + .unwrap_or(max); + if pos < max { + address_book_name = Some(args[pos + 1].to_owned()); + } + let mut address_book_tag = None; + let pos = args + .iter() + .position(|x| x == "--address_book_tag") + .unwrap_or(max); + if pos < max { + address_book_tag = Some(args[pos + 1].to_owned()); + } let mut body = serde_json::json!({ "id": id, "uuid": uuid, }); let header = "Authorization: Bearer ".to_owned() + &token; if user_name.is_none() && strategy_name.is_none() { - println!("--user_name or --strategy_name is required!"); + println!( + "--user_name or --strategy_name or address_book_name is required!" + ); } else { if let Some(name) = user_name { body["user_name"] = serde_json::json!(name); @@ -391,6 +409,12 @@ pub fn core_main() -> Option> { if let Some(name) = strategy_name { body["strategy_name"] = serde_json::json!(name); } + if let Some(name) = address_book_name { + body["address_book_name"] = serde_json::json!(name); + if let Some(name) = address_book_tag { + body["address_book_tag"] = serde_json::json!(name); + } + } let url = crate::ui_interface::get_api_server() + "/api/devices/cli"; match crate::post_request_sync(url, body.to_string(), &header) { Err(err) => println!("{}", err), From 2266fde26f609586641f3093a1230c57cade73d8 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Sun, 4 Aug 2024 19:31:13 +0800 Subject: [PATCH 056/541] fix address_book_name cli --- src/core_main.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core_main.rs b/src/core_main.rs index 942066ae3584..bcd22aabe56e 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -398,9 +398,9 @@ pub fn core_main() -> Option> { "uuid": uuid, }); let header = "Authorization: Bearer ".to_owned() + &token; - if user_name.is_none() && strategy_name.is_none() { + if user_name.is_none() && strategy_name.is_none() && address_book_name.is_none() { println!( - "--user_name or --strategy_name or address_book_name is required!" + "--user_name or --strategy_name or --address_book_name is required!" ); } else { if let Some(name) = user_name { From b3e1c8a90770937350b0e825292bd1ede9acb861 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Mon, 5 Aug 2024 17:49:48 +0800 Subject: [PATCH 057/541] Refact/msi more install options (#8949) * refact: msi, more install options Signed-off-by: fufesou * refact: msi, reg values on upgrade/modify Signed-off-by: fufesou * fix: msi, silent repair/upgrade, RemoveInstallFolder() Signed-off-by: fufesou * Options support both 1/0 and Y/N Signed-off-by: fufesou * refact: msi, preprocess, open file with explicit encoding Signed-off-by: fufesou * fix: msi, read previous options Signed-off-by: fufesou * fix: mis, install folder, read previous option Signed-off-by: fufesou * Comment on Control -> Checkbox Signed-off-by: fufesou * fix: UI, checkbox options, read previous values Signed-off-by: fufesou * fix: shortcuts options, init state Signed-off-by: fufesou * fix: shortcuts, init state Signed-off-by: fufesou * Better shortcuts property conditions Signed-off-by: fufesou --------- Signed-off-by: fufesou --- res/msi/CustomActions/CustomActions.cpp | 15 +++- res/msi/Package/Components/Folders.wxs | 20 ++++- res/msi/Package/Components/Regs.wxs | 12 +-- res/msi/Package/Components/RustDesk.wxs | 28 +++--- .../Package/Fragments/ShortcutProperties.wxs | 26 ++++-- res/msi/Package/Language/Package.en-us.wxl | 3 + res/msi/Package/Package.wxs | 11 ++- res/msi/Package/UI/AnotherApp.wxs | 2 +- res/msi/Package/UI/MyInstallDirDlg.wxs | 31 +++++++ res/msi/Package/UI/MyInstallDlg.wxs | 87 +++++++++++++++++++ res/msi/preprocess.py | 20 ++--- 11 files changed, 209 insertions(+), 46 deletions(-) create mode 100644 res/msi/Package/UI/MyInstallDirDlg.wxs create mode 100644 res/msi/Package/UI/MyInstallDlg.wxs diff --git a/res/msi/CustomActions/CustomActions.cpp b/res/msi/CustomActions/CustomActions.cpp index 91ba7e03c238..afe06fdbb0b1 100644 --- a/res/msi/CustomActions/CustomActions.cpp +++ b/res/msi/CustomActions/CustomActions.cpp @@ -31,6 +31,11 @@ UINT __stdcall CustomActionHello( return WcaFinalize(er); } +// CAUTION: We can't simply remove the install folder here, because silent repair/upgrade will fail. +// `RemoveInstallFolder()` is a deferred custom action, it will be executed after the files are copied. +// `msiexec /i package.msi /qn` +// +// So we need to delete the files separately in install folder. UINT __stdcall RemoveInstallFolder( __in MSIHANDLE hInstall) { @@ -41,6 +46,7 @@ UINT __stdcall RemoveInstallFolder( LPWSTR installFolder = NULL; LPWSTR pwz = NULL; LPWSTR pwzData = NULL; + WCHAR runtimeBroker[1024] = { 0, }; hr = WcaInitialize(hInstall, "RemoveInstallFolder"); ExitOnFailure(hr, "Failed to initialize"); @@ -52,21 +58,22 @@ UINT __stdcall RemoveInstallFolder( hr = WcaReadStringFromCaData(&pwz, &installFolder); ExitOnFailure(hr, "failed to read database key from custom action data: %ls", pwz); + StringCchPrintfW(runtimeBroker, sizeof(runtimeBroker) / sizeof(runtimeBroker[0]), L"%ls\\RuntimeBroker_rustdesk.exe", installFolder); + SHFILEOPSTRUCTW fileOp; ZeroMemory(&fileOp, sizeof(SHFILEOPSTRUCT)); - fileOp.wFunc = FO_DELETE; - fileOp.pFrom = installFolder; + fileOp.pFrom = runtimeBroker; fileOp.fFlags = FOF_NOCONFIRMATION | FOF_SILENT; nResult = SHFileOperationW(&fileOp); if (nResult == 0) { - WcaLog(LOGMSG_STANDARD, "The directory \"%ls\" has been deleted.", installFolder); + WcaLog(LOGMSG_STANDARD, "The external file \"%ls\" has been deleted.", runtimeBroker); } else { - WcaLog(LOGMSG_STANDARD, "The directory \"%ls\" has not been deleted, error code: 0x%02X. Please refer to https://learn.microsoft.com/en-us/windows/win32/api/shellapi/nf-shellapi-shfileoperationa for the error codes.", installFolder, nResult); + WcaLog(LOGMSG_STANDARD, "The external file \"%ls\" has not been deleted, error code: 0x%02X. Please refer to https://learn.microsoft.com/en-us/windows/win32/api/shellapi/nf-shellapi-shfileoperationa for the error codes.", runtimeBroker, nResult); } LExit: diff --git a/res/msi/Package/Components/Folders.wxs b/res/msi/Package/Components/Folders.wxs index c39951019223..de9edb7f38f3 100644 --- a/res/msi/Package/Components/Folders.wxs +++ b/res/msi/Package/Components/Folders.wxs @@ -3,8 +3,26 @@ + + + + + + + + + + + + + + + + - + diff --git a/res/msi/Package/Components/Regs.wxs b/res/msi/Package/Components/Regs.wxs index 23d4b6b8d5b4..33d587b1e5a8 100644 --- a/res/msi/Package/Components/Regs.wxs +++ b/res/msi/Package/Components/Regs.wxs @@ -5,16 +5,16 @@ - - + + - + - + @@ -22,7 +22,7 @@ - + @@ -36,7 +36,7 @@ - + diff --git a/res/msi/Package/Components/RustDesk.wxs b/res/msi/Package/Components/RustDesk.wxs index b2049f4e8a02..c17d60edb792 100644 --- a/res/msi/Package/Components/RustDesk.wxs +++ b/res/msi/Package/Components/RustDesk.wxs @@ -4,7 +4,7 @@ - + @@ -12,10 +12,10 @@ - - - - + + + + @@ -29,7 +29,7 @@ - + @@ -88,8 +88,8 @@ - - + + - + diff --git a/res/msi/Package/Fragments/ShortcutProperties.wxs b/res/msi/Package/Fragments/ShortcutProperties.wxs index e800262fb10f..95bd4aaa370c 100644 --- a/res/msi/Package/Fragments/ShortcutProperties.wxs +++ b/res/msi/Package/Fragments/ShortcutProperties.wxs @@ -25,11 +25,25 @@ - - + + - - + + + + + + + + + + + + + + + + @@ -45,8 +59,8 @@ - - + + diff --git a/res/msi/Package/Language/Package.en-us.wxl b/res/msi/Package/Language/Package.en-us.wxl index 517bde2af8c7..1bd3986ddbb9 100644 --- a/res/msi/Package/Language/Package.en-us.wxl +++ b/res/msi/Package/Language/Package.en-us.wxl @@ -49,4 +49,7 @@ This file contains the declaration of all the localizable strings. + + + diff --git a/res/msi/Package/Package.wxs b/res/msi/Package/Package.wxs index e86494e8517f..bdd8471cfc0c 100644 --- a/res/msi/Package/Package.wxs +++ b/res/msi/Package/Package.wxs @@ -18,11 +18,11 @@ - + - + @@ -40,14 +40,17 @@ - + - + + + + diff --git a/res/msi/Package/UI/AnotherApp.wxs b/res/msi/Package/UI/AnotherApp.wxs index ea46812f7767..168d1b10e06b 100644 --- a/res/msi/Package/UI/AnotherApp.wxs +++ b/res/msi/Package/UI/AnotherApp.wxs @@ -1,7 +1,7 @@ -

+ diff --git a/res/msi/Package/UI/MyInstallDirDlg.wxs b/res/msi/Package/UI/MyInstallDirDlg.wxs new file mode 100644 index 000000000000..7c554a1215de --- /dev/null +++ b/res/msi/Package/UI/MyInstallDirDlg.wxs @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/msi/Package/UI/MyInstallDlg.wxs b/res/msi/Package/UI/MyInstallDlg.wxs new file mode 100644 index 000000000000..bf59d569cac7 --- /dev/null +++ b/res/msi/Package/UI/MyInstallDlg.wxs @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/msi/preprocess.py b/res/msi/preprocess.py index a5251c1cef62..9a43e9da6a9d 100644 --- a/res/msi/preprocess.py +++ b/res/msi/preprocess.py @@ -90,7 +90,7 @@ def make_parser(): def read_lines_and_start_index(file_path, tag_start, tag_end): - with open(file_path, "r") as f: + with open(file_path, "r", encoding="utf-8") as f: lines = f.readlines() index_start = -1 index_end = -1 @@ -180,11 +180,11 @@ def func(lines, index_start): def replace_app_name_in_langs(app_name): langs_dir = Path(sys.argv[0]).parent.joinpath("Package/Language") for file_path in langs_dir.glob("*.wxl"): - with open(file_path, "r") as f: + with open(file_path, "r", encoding="utf-8") as f: lines = f.readlines() for i, line in enumerate(lines): lines[i] = line.replace("RustDesk", app_name) - with open(file_path, "w") as f: + with open(file_path, "w", encoding="utf-8") as f: f.writelines(lines) @@ -301,7 +301,7 @@ def func(lines, index_start): f'{indent}\n' ) lines_new.append( - f'{indent}\n' + f'{indent}\n' ) lines_new.append( f'{indent}\n' @@ -314,7 +314,7 @@ def func(lines, index_start): f'{indent}\n' ) lines_new.append( - f'{indent}\n' + f'{indent}\n' ) lines_new.append( f'{indent}\n' @@ -420,7 +420,7 @@ def gen_content_between_tags(filename, tag_start, tag_end, func): func(lines, index_start) - with open(target_file, "w") as f: + with open(target_file, "w", encoding="utf-8") as f: f.writelines(lines) return True @@ -480,19 +480,19 @@ def update_license_file(app_name): if app_name == "RustDesk": return license_file = Path(sys.argv[0]).parent.joinpath("Package/License.rtf") - with open(license_file, "r") as f: + with open(license_file, "r", encoding="utf-8") as f: license_content = f.read() license_content = license_content.replace("website rustdesk.com and other ", "") license_content = license_content.replace("RustDesk", app_name) license_content = re.sub("Purslane Ltd", app_name, license_content, flags=re.IGNORECASE) - with open(license_file, "w") as f: + with open(license_file, "w", encoding="utf-8") as f: f.write(license_content) def replace_component_guids_in_wxs(): langs_dir = Path(sys.argv[0]).parent.joinpath("Package") for file_path in langs_dir.glob("**/*.wxs"): - with open(file_path, "r") as f: + with open(file_path, "r", encoding="utf-8") as f: lines = f.readlines() # @@ -501,7 +501,7 @@ def replace_component_guids_in_wxs(): if match: lines[i] = re.sub(r'Guid="[^"]+"', f'Guid="{uuid.uuid4()}"', line) - with open(file_path, "w") as f: + with open(file_path, "w", encoding="utf-8") as f: f.writelines(lines) From 2662abc5a39b25d76568f3278de1fb4fd62b59b0 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Tue, 6 Aug 2024 00:28:31 +0800 Subject: [PATCH 058/541] fix: reset voice call state, on conn (#8961) Signed-off-by: fufesou --- flutter/lib/models/model.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index cf93fde42e4b..15510377673b 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -723,6 +723,8 @@ class FfiModel with ChangeNotifier { /// Handle the peer info event based on [evt]. handlePeerInfo(Map evt, String peerId, bool isCache) async { + parent.target?.chatModel.voiceCallStatus.value = VoiceCallStatus.notStarted; + // This call is to ensuer the keyboard mode is updated depending on the peer version. parent.target?.inputModel.updateKeyboardMode(); From 421ddc001603b014a45d8492894989816e4e6f66 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Tue, 6 Aug 2024 12:11:03 +0800 Subject: [PATCH 059/541] fix https://github.com/rustdesk/rustdesk/issues/4863 --- src/core_main.rs | 13 ++++++++++++- src/platform/linux.rs | 39 ++++++++++++++++++++++----------------- 2 files changed, 34 insertions(+), 18 deletions(-) diff --git a/src/core_main.rs b/src/core_main.rs index bcd22aabe56e..902c816e534b 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -261,6 +261,16 @@ pub fn core_main() -> Option> { return None; } else if args[0] == "--server" { log::info!("start --server with user {}", crate::username()); + #[cfg(target_os = "linux")] + { + hbb_common::allow_err!(crate::platform::check_autostart_config()); + std::process::Command::new("pkill") + .arg("-f") + .arg(&format!("%s --tray", crate::get_app_name().to_lowercase())) + .status() + .ok(); + allow_err!(crate::crate::platform::run_as_user(vec!["--tray"])); + } #[cfg(windows)] crate::privacy_mode::restore_reg_connectivity(true); #[cfg(any(target_os = "linux", target_os = "windows"))] @@ -398,7 +408,8 @@ pub fn core_main() -> Option> { "uuid": uuid, }); let header = "Authorization: Bearer ".to_owned() + &token; - if user_name.is_none() && strategy_name.is_none() && address_book_name.is_none() { + if user_name.is_none() && strategy_name.is_none() && address_book_name.is_none() + { println!( "--user_name or --strategy_name or --address_book_name is required!" ); diff --git a/src/platform/linux.rs b/src/platform/linux.rs index f936bfb8cc11..90e2f52ca090 100644 --- a/src/platform/linux.rs +++ b/src/platform/linux.rs @@ -730,7 +730,8 @@ pub fn block_input(_v: bool) -> (bool, String) { pub fn is_installed() -> bool { if let Ok(p) = std::env::current_exe() { - p.to_str().unwrap_or_default().starts_with("/usr") || p.to_str().unwrap_or_default().starts_with("/nix/store") + p.to_str().unwrap_or_default().starts_with("/usr") + || p.to_str().unwrap_or_default().starts_with("/nix/store") } else { false } @@ -1413,22 +1414,26 @@ pub fn check_autostart_config() -> ResultType<()> { let app_name = crate::get_app_name().to_lowercase(); let path = format!("{home}/.config/autostart"); let file = format!("{path}/{app_name}.desktop"); - std::fs::create_dir_all(&path).ok(); - if !Path::new(&file).exists() { - // write text to the desktop file - let mut file = std::fs::File::create(&file)?; - file.write_all( - format!( - " -[Desktop Entry] -Type=Application -Exec={app_name} --tray -NoDisplay=false - " - ) - .as_bytes(), - )?; - } + // https://github.com/rustdesk/rustdesk/issues/4863 + std::fs::remove_file(&file).ok(); + /* + std::fs::create_dir_all(&path).ok(); + if !Path::new(&file).exists() { + // write text to the desktop file + let mut file = std::fs::File::create(&file)?; + file.write_all( + format!( + " + [Desktop Entry] + Type=Application + Exec={app_name} --tray + NoDisplay=false + " + ) + .as_bytes(), + )?; + } + */ Ok(()) } From 877b3e2ce58d25f1f02821d45e86bbf5ec431e0e Mon Sep 17 00:00:00 2001 From: rustdesk Date: Tue, 6 Aug 2024 12:19:06 +0800 Subject: [PATCH 060/541] fix ci --- src/core_main.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core_main.rs b/src/core_main.rs index 902c816e534b..2c4b22466ee5 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -266,10 +266,10 @@ pub fn core_main() -> Option> { hbb_common::allow_err!(crate::platform::check_autostart_config()); std::process::Command::new("pkill") .arg("-f") - .arg(&format!("%s --tray", crate::get_app_name().to_lowercase())) + .arg(&format!("{} --tray", crate::get_app_name().to_lowercase())) .status() .ok(); - allow_err!(crate::crate::platform::run_as_user(vec!["--tray"])); + hbb_common::allow_err!(crate::crate::platform::run_as_user(vec!["--tray"])); } #[cfg(windows)] crate::privacy_mode::restore_reg_connectivity(true); From 5a2121501d508274d792561db608c2c3f0e86291 Mon Sep 17 00:00:00 2001 From: 21pages Date: Tue, 6 Aug 2024 12:36:39 +0800 Subject: [PATCH 061/541] fix build (#8964) Signed-off-by: 21pages --- src/core_main.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/core_main.rs b/src/core_main.rs index 2c4b22466ee5..c7e4d1f00885 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -269,7 +269,11 @@ pub fn core_main() -> Option> { .arg(&format!("{} --tray", crate::get_app_name().to_lowercase())) .status() .ok(); - hbb_common::allow_err!(crate::crate::platform::run_as_user(vec!["--tray"])); + hbb_common::allow_err!(crate::platform::run_as_user( + vec!["--tray"], + None, + None::<(&str, &str)>, + )); } #[cfg(windows)] crate::privacy_mode::restore_reg_connectivity(true); From 51b250435dcdc0f6b6252d488897e10348526de6 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Tue, 6 Aug 2024 17:07:05 +0800 Subject: [PATCH 062/541] refact: audio input, combobox instead of radio (#8965) Signed-off-by: fufesou --- .../desktop/pages/desktop_setting_page.dart | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 79c18c521183..ba2c14d6997f 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -515,16 +515,16 @@ class _GeneralState extends State<_General> { } builder(devices, currentDevice, setDevice) { - return _Card(title: 'Audio Input Device', children: [ - ...devices.map((device) => _Radio(context, - value: device, - groupValue: currentDevice, - autoNewLine: false, - label: device, onChanged: (value) { - setDevice(value); - setState(() {}); - })) - ]); + final child = ComboBox( + keys: devices, + values: devices, + initialKey: currentDevice, + onChanged: (key) async { + setDevice(key); + setState(() {}); + }, + ).marginOnly(left: _kContentHMargin); + return _Card(title: 'Audio Input Device', children: [child]); } return AudioInput(builder: builder, isCm: false, isVoiceCall: false); From 96edca8f74e195896db9f9929d2aed3100eac47f Mon Sep 17 00:00:00 2001 From: 21pages Date: Tue, 6 Aug 2024 18:19:35 +0800 Subject: [PATCH 063/541] update sysinfo rlim_max, which causing debian 13 pkexec not work (#8968) * update sysinfo rlim_max, which causing debian 13 pkexec not work Signed-off-by: 21pages * Update Cargo.toml --------- Signed-off-by: 21pages Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com> --- Cargo.lock | 2 +- libs/hbb_common/Cargo.toml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8f40d997af70..b5d653579484 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6229,7 +6229,7 @@ dependencies = [ [[package]] name = "sysinfo" version = "0.29.10" -source = "git+https://github.com/rustdesk-org/sysinfo#f45dcc6510d48c3a1401c5a33eedccc8899f67b2" +source = "git+https://github.com/rustdesk-org/sysinfo?branch=rlim_max#90b1705d909a4902dbbbdea37ee64db17841077d" dependencies = [ "cfg-if 1.0.0", "core-foundation-sys 0.8.6 (registry+https://github.com/rust-lang/crates.io-index)", diff --git a/libs/hbb_common/Cargo.toml b/libs/hbb_common/Cargo.toml index 048c8075fbb7..259d01e9dd4b 100644 --- a/libs/hbb_common/Cargo.toml +++ b/libs/hbb_common/Cargo.toml @@ -37,8 +37,8 @@ libc = "0.2" dlopen = "0.1" toml = "0.7" uuid = { version = "1.3", features = ["v4"] } -# crash, versions >= 0.29.1 are affected by #GuillaumeGomez/sysinfo/1052 -sysinfo = { git = "https://github.com/rustdesk-org/sysinfo" } +# new sysinfo issue: https://github.com/rustdesk/rustdesk/pull/6330#issuecomment-2270871442 +sysinfo = { git = "https://github.com/rustdesk-org/sysinfo", branch = "rlim_max" } thiserror = "1.0" httparse = "1.5" base64 = "0.22" From 2f432e941ddae05ce8a0603ec29791c6b79d2f17 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Wed, 7 Aug 2024 01:08:36 +0800 Subject: [PATCH 064/541] hide-tray option --- libs/hbb_common/src/config.rs | 4 +++- src/client.rs | 6 +++--- src/common.rs | 2 +- src/flutter_ffi.rs | 2 +- src/hbbs_http/sync.rs | 6 +++--- src/rendezvous_mediator.rs | 4 ++-- src/tray.rs | 10 ++++++++++ src/ui_interface.rs | 5 +++-- 8 files changed, 26 insertions(+), 13 deletions(-) diff --git a/libs/hbb_common/src/config.rs b/libs/hbb_common/src/config.rs index ff6de4430a28..0a884e1f8b70 100644 --- a/libs/hbb_common/src/config.rs +++ b/libs/hbb_common/src/config.rs @@ -69,7 +69,7 @@ lazy_static::lazy_static! { pub static ref DEFAULT_LOCAL_SETTINGS: RwLock> = Default::default(); pub static ref OVERWRITE_LOCAL_SETTINGS: RwLock> = Default::default(); pub static ref HARD_SETTINGS: RwLock> = Default::default(); - pub static ref BUILDIN_SETTINGS: RwLock> = Default::default(); + pub static ref BUILTIN_SETTINGS: RwLock> = Default::default(); } lazy_static::lazy_static! { @@ -2114,6 +2114,7 @@ pub mod keys { pub const OPTION_HIDE_USERNAME_ON_CARD: &str = "hide-username-on-card"; pub const OPTION_HIDE_HELP_CARDS: &str = "hide-help-cards"; pub const OPTION_DEFAULT_CONNECT_PASSWORD: &str = "default-connect-password"; + pub const OPTION_HIDE_TRAY: &str = "hide-tray"; // flutter local options pub const OPTION_FLUTTER_REMOTE_MENUBAR_STATE: &str = "remoteMenubarState"; @@ -2256,6 +2257,7 @@ pub mod keys { OPTION_HIDE_USERNAME_ON_CARD, OPTION_HIDE_HELP_CARDS, OPTION_DEFAULT_CONNECT_PASSWORD, + OPTION_HIDE_TRAY, ]; } diff --git a/src/client.rs b/src/client.rs index 48e8033b9ad8..ec85f4807467 100644 --- a/src/client.rs +++ b/src/client.rs @@ -65,7 +65,7 @@ use crate::{ check_port, common::input::{MOUSE_BUTTON_LEFT, MOUSE_BUTTON_RIGHT, MOUSE_TYPE_DOWN, MOUSE_TYPE_UP}, create_symmetric_key_msg, decode_id_pk, get_rs_pk, is_keyboard_mode_supported, secure_tcp, - ui_interface::{get_buildin_option, use_texture_render}, + ui_interface::{get_builtin_option, use_texture_render}, ui_session_interface::{InvokeUiSession, Session}, }; @@ -2137,7 +2137,7 @@ impl LoginConfigHandler { } else { (my_id, self.id.clone()) }; - let mut display_name = get_buildin_option(config::keys::OPTION_DISPLAY_NAME); + let mut display_name = get_builtin_option(config::keys::OPTION_DISPLAY_NAME); if display_name.is_empty() { display_name = serde_json::from_str::(&LocalConfig::get_option("user_info")) @@ -2920,7 +2920,7 @@ pub async fn handle_hash( if password.is_empty() { let p = - crate::ui_interface::get_buildin_option(config::keys::OPTION_DEFAULT_CONNECT_PASSWORD); + crate::ui_interface::get_builtin_option(config::keys::OPTION_DEFAULT_CONNECT_PASSWORD); if !p.is_empty() { let mut hasher = Sha256::new(); hasher.update(p.clone()); diff --git a/src/common.rs b/src/common.rs index 740b5d9e375a..108d25659f27 100644 --- a/src/common.rs +++ b/src/common.rs @@ -1359,7 +1359,7 @@ fn read_custom_client_advanced_settings( } else { config::DEFAULT_SETTINGS.write().unwrap() }; - let mut buildin_settings = config::BUILDIN_SETTINGS.write().unwrap(); + let mut buildin_settings = config::BUILTIN_SETTINGS.write().unwrap(); if let Some(settings) = settings.as_object() { for (k, v) in settings { diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 08e79cbea94b..cceec4cd1496 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -2220,7 +2220,7 @@ pub fn main_get_hard_option(key: String) -> SyncReturn { } pub fn main_get_buildin_option(key: String) -> SyncReturn { - SyncReturn(get_buildin_option(&key)) + SyncReturn(get_builtin_option(&key)) } pub fn main_check_hwcodec() { diff --git a/src/hbbs_http/sync.rs b/src/hbbs_http/sync.rs index f2717a42ca19..91c18c4b206b 100644 --- a/src/hbbs_http/sync.rs +++ b/src/hbbs_http/sync.rs @@ -5,7 +5,7 @@ use std::{ }; #[cfg(not(any(target_os = "ios")))] -use crate::{ui_interface::get_buildin_option, Connection}; +use crate::{ui_interface::get_builtin_option, Connection}; use hbb_common::{ config::{keys, Config, LocalConfig}, tokio::{self, sync::broadcast, time::Instant}, @@ -91,11 +91,11 @@ async fn start_hbbs_sync_async() { if !ab_tag.is_empty() { v[keys::OPTION_PRESET_ADDRESS_BOOK_TAG] = json!(ab_tag); } - let username = get_buildin_option(keys::OPTION_PRESET_USERNAME); + let username = get_builtin_option(keys::OPTION_PRESET_USERNAME); if !username.is_empty() { v[keys::OPTION_PRESET_USERNAME] = json!(username); } - let strategy_name = get_buildin_option(keys::OPTION_PRESET_STRATEGY_NAME); + let strategy_name = get_builtin_option(keys::OPTION_PRESET_STRATEGY_NAME); if !strategy_name.is_empty() { v[keys::OPTION_PRESET_STRATEGY_NAME] = json!(strategy_name); } diff --git a/src/rendezvous_mediator.rs b/src/rendezvous_mediator.rs index 65bb6a2b3ed5..4ae222966e97 100644 --- a/src/rendezvous_mediator.rs +++ b/src/rendezvous_mediator.rs @@ -32,7 +32,7 @@ use hbb_common::{ use crate::{ check_port, server::{check_zombie, new as new_server, ServerPtr}, - ui_interface::get_buildin_option, + ui_interface::get_builtin_option, }; type Message = RendezvousMessage; @@ -391,7 +391,7 @@ impl RendezvousMediator { }; if (cfg!(debug_assertions) && option_env!("TEST_TCP").is_some()) || is_http_proxy - || get_buildin_option(config::keys::OPTION_DISABLE_UDP) == "Y" + || get_builtin_option(config::keys::OPTION_DISABLE_UDP) == "Y" { Self::start_tcp(server, host).await } else { diff --git a/src/tray.rs b/src/tray.rs index 18d6d7e91ad7..4b30c7e089ac 100644 --- a/src/tray.rs +++ b/src/tray.rs @@ -9,6 +9,16 @@ use std::sync::{Arc, Mutex}; use std::time::Duration; pub fn start_tray() { + if crate::ui_interface::get_builtin_option(hbb_common::config::keys::OPTION_HIDE_TRAY) == "Y" { + if cfg!(target_os = "macos") { + crate::platform::macos::hide_dock(); + loop { + std::thread::sleep(std::time::Duration::from_secs(1)); + } + } else { + return; + } + } allow_err!(make_tray()); } diff --git a/src/ui_interface.rs b/src/ui_interface.rs index a192265df07a..443f6fa4276f 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -206,8 +206,8 @@ pub fn get_hard_option(key: String) -> String { } #[inline] -pub fn get_buildin_option(key: &str) -> String { - config::BUILDIN_SETTINGS +pub fn get_builtin_option(key: &str) -> String { + config::BUILTIN_SETTINGS .read() .unwrap() .get(key) @@ -781,6 +781,7 @@ pub fn http_request(url: String, method: String, body: Option, header: S current_request.lock().unwrap().insert(url, res); }); } + #[inline] pub fn get_async_http_status(url: String) -> Option { match ASYNC_HTTP_STATUS.lock().unwrap().get(&url) { From 025cdfa25b0bb3d8a169b29829bc54eaf69eb70f Mon Sep 17 00:00:00 2001 From: rustdesk Date: Wed, 7 Aug 2024 01:19:29 +0800 Subject: [PATCH 065/541] fix ci --- src/tray.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/tray.rs b/src/tray.rs index 4b30c7e089ac..39d650662c78 100644 --- a/src/tray.rs +++ b/src/tray.rs @@ -10,12 +10,15 @@ use std::time::Duration; pub fn start_tray() { if crate::ui_interface::get_builtin_option(hbb_common::config::keys::OPTION_HIDE_TRAY) == "Y" { - if cfg!(target_os = "macos") { + #[cfg(target_os = "macos")] + { crate::platform::macos::hide_dock(); loop { std::thread::sleep(std::time::Duration::from_secs(1)); } - } else { + } + #[cfg(not(target_os = "macos"))] + { return; } } From bc6ce6c7eeadb6a285241c7b9c3450ff4e78c7c5 Mon Sep 17 00:00:00 2001 From: jkh0kr Date: Wed, 7 Aug 2024 11:12:43 +0900 Subject: [PATCH 066/541] Update ko.rs (#8974) Additional Korean translation --- src/lang/ko.rs | 94 +++++++++++++++++++++++++------------------------- 1 file changed, 47 insertions(+), 47 deletions(-) diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 3d96ce0f2dfa..7f7e80d6be12 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -585,52 +585,52 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please select the session you want to connect to", "연결하려는 세션을 선택하세요."), ("powered_by_me", "RustDesk 제공"), ("outgoing_only_desk_tip", "이것은 맞춤형 버전입니다.\n다른 장치에 연결할 수 있지만 다른 장치는 귀하의 장치에 연결할 수 없습니다."), - ("preset_password_warning", ""), - ("Security Alert", ""), - ("My address book", ""), - ("Personal", ""), - ("Owner", ""), - ("Set shared password", ""), - ("Exist in", ""), - ("Read-only", ""), - ("Read/Write", ""), - ("Full Control", ""), - ("share_warning_tip", ""), - ("Everyone", ""), - ("ab_web_console_tip", ""), - ("allow-only-conn-window-open-tip", ""), - ("no_need_privacy_mode_no_physical_displays_tip", ""), - ("Follow remote cursor", ""), - ("Follow remote window focus", ""), - ("default_proxy_tip", ""), - ("no_audio_input_device_tip", ""), - ("Incoming", ""), - ("Outgoing", ""), - ("Clear Wayland screen selection", ""), - ("clear_Wayland_screen_selection_tip", ""), - ("confirm_clear_Wayland_screen_selection_tip", ""), - ("android_new_voice_call_tip", ""), - ("texture_render_tip", ""), - ("Use texture rendering", ""), - ("Floating window", ""), - ("floating_window_tip", ""), - ("Keep screen on", ""), - ("Never", ""), - ("During controlled", ""), - ("During service is on", ""), - ("Capture screen using DirectX", ""), - ("Back", ""), - ("Apps", ""), - ("Volume up", ""), - ("Volume down", ""), - ("Power", ""), - ("Telegram bot", ""), - ("enable-bot-tip", ""), - ("enable-bot-desc", ""), - ("cancel-2fa-confirm-tip", ""), - ("cancel-bot-confirm-tip", ""), - ("About RustDesk", ""), - ("Send clipboard keystrokes", ""), - ("network_error_tip", ""), + ("preset_password_warning", "이 맞춤형 에디션은 미리 설정된 비밀번호가 포함되어 있습니다. 이 비밀번호를 아는 사람은 누구나 귀하의 장치를 완전히 제어할 수 있습니다. 이 상황을 예상하지 못했다면, 즉시 소프트웨어를 삭제하시기 바랍니다."), + ("Security Alert", "보안 경고"), + ("My address book", "내 주소록"), + ("Personal", "개인"), + ("Owner", "소유자"), + ("Set shared password", "공유 암호 설정"), + ("Exist in", "존재함"), + ("Read-only", "읽기전용"), + ("Read/Write", "읽기/쓰기"), + ("Full Control", "전체 제어"), + ("share_warning_tip", "위의 항목들은 다른 사람들과 공유되며, 다른 사람들이 볼 수 있습니다."), + ("Everyone", "모두"), + ("ab_web_console_tip", "웹 콘솔에 대해 더 알아보기"), + ("allow-only-conn-window-open-tip", "RustDesk 창이 열려 있는 경우에만 연결을 허용하십시오."), + ("no_need_privacy_mode_no_physical_displays_tip", "물리적 디스플레이가 없으므로 프라이버시 모드를 사용할 필요가 없습니다."), + ("Follow remote cursor", "원격 커서 따르기"), + ("Follow remote window focus", "원격 창 포커스 따르기"), + ("default_proxy_tip", "기본 프로토콜과 포트는 Socks5와 1080입니다."), + ("no_audio_input_device_tip", "오디오 입력 장치를 찾을 수 없습니다."), + ("Incoming", "수신중"), + ("Outgoing", "발신중"), + ("Clear Wayland screen selection", "Wayland 화면 선택 취소"), + ("clear_Wayland_screen_selection_tip", "화면 선택을 취소한 후 다시 공유할 화면을 선택할 수 있습니다."), + ("confirm_clear_Wayland_screen_selection_tip", "Wayland 화면 선택을 정말 취소하시겠습니까?"), + ("android_new_voice_call_tip", "새로운 음성 통화 요청이 있습니다. 수락하면 오디오가 음성 통신으로 전환됩니다."), + ("texture_render_tip", "텍스처 렌더링을 사용하면 이미지가 더 부드러워집니다. 렌더링 문제가 발생하면 이 옵션을 비활성화해 보세요."), + ("Use texture rendering", "텍스처 렌더링 사용"), + ("Floating window", "플로팅 윈도우"), + ("floating_window_tip", "RustDesk 백그라운드 서비스를 유지하는 것이 좋습니다."), + ("Keep screen on", "화면 켜짐 유지"), + ("Never", "없음"), + ("During controlled", "제어되는 동안"), + ("During service is on", "서비스가 켜져 있는 동안"), + ("Capture screen using DirectX", "다이렉트X를 사용한 화면 캡처"), + ("Back", "뒤로"), + ("Apps", "앱"), + ("Volume up", "볼륨 증가"), + ("Volume down", "볼륨 감소"), + ("Power", "파워"), + ("Telegram bot", "텔레그램 봇"), + ("enable-bot-tip", "이 기능을 활성화하면 봇으로부터 2FA 코드를 받을 수 있습니다. 또한 연결 알림으로도 작동할 수 있습니다."), + ("enable-bot-desc", "1. @BotFather와 채팅을 시작하세요.\n2. \"/newbot\" 명령어를 보내세요. 토큰을 받게 됩니다.\n3. 새로 생성된 봇과 채팅을 시작하고 \"/hello\" 등의 명령어를 보내 봇을 활성화하세요."), + ("cancel-2fa-confirm-tip", "2FA를 정말 취소하시겠습니까?"), + ("cancel-bot-confirm-tip", "텔레그램 봇을 정말 삭제하시겠습니까?"), + ("About RustDesk", "RustDesk 대하여"), + ("Send clipboard keystrokes", "클립보드 키 입력 전송"), + ("network_error_tip", "네트워크 연결을 확인한 후 다시 시도하세요."), ].iter().cloned().collect(); } From 76d5a8b20587329c86e53ca5f755fa7062174aef Mon Sep 17 00:00:00 2001 From: 21pages Date: Wed, 7 Aug 2024 16:21:38 +0800 Subject: [PATCH 067/541] unlock with PIN (#8977) * add custom password to unlock settings * If not set, use admin password; if set, use custom settings password. * At least 4 characters. * Set with gui or command line. Signed-off-by: 21pages * Update cn.rs --------- Signed-off-by: 21pages Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com> --- flutter/lib/common/widgets/dialog.dart | 99 ++++++++++++++++++- .../desktop/pages/desktop_setting_page.dart | 46 ++++++++- flutter/lib/web/bridge.dart | 8 ++ libs/hbb_common/src/config.rs | 31 +++++- src/core_main.rs | 14 +++ src/flutter_ffi.rs | 8 ++ src/ipc.rs | 35 +++++++ src/lang/ar.rs | 4 + src/lang/be.rs | 4 + src/lang/bg.rs | 4 + src/lang/ca.rs | 4 + src/lang/cn.rs | 4 + src/lang/cs.rs | 4 + src/lang/da.rs | 4 + src/lang/de.rs | 4 + src/lang/el.rs | 4 + src/lang/eo.rs | 4 + src/lang/es.rs | 4 + src/lang/et.rs | 4 + src/lang/eu.rs | 4 + src/lang/fa.rs | 4 + src/lang/fr.rs | 4 + src/lang/he.rs | 4 + src/lang/hr.rs | 4 + src/lang/hu.rs | 4 + src/lang/id.rs | 4 + src/lang/it.rs | 6 +- src/lang/ja.rs | 4 + src/lang/ko.rs | 4 + src/lang/kz.rs | 4 + src/lang/lt.rs | 4 + src/lang/lv.rs | 4 + src/lang/nb.rs | 4 + src/lang/nl.rs | 4 + src/lang/pl.rs | 4 + src/lang/pt_PT.rs | 4 + src/lang/ptbr.rs | 4 + src/lang/ro.rs | 4 + src/lang/ru.rs | 4 + src/lang/sk.rs | 4 + src/lang/sl.rs | 4 + src/lang/sq.rs | 4 + src/lang/sr.rs | 4 + src/lang/sv.rs | 4 + src/lang/template.rs | 4 + src/lang/th.rs | 4 + src/lang/tr.rs | 4 + src/lang/tw.rs | 4 + src/lang/uk.rs | 4 + src/lang/vn.rs | 4 + src/ui_interface.rs | 19 ++++ 51 files changed, 425 insertions(+), 9 deletions(-) diff --git a/flutter/lib/common/widgets/dialog.dart b/flutter/lib/common/widgets/dialog.dart index f140a68b0779..58f85feb09e8 100644 --- a/flutter/lib/common/widgets/dialog.dart +++ b/flutter/lib/common/widgets/dialog.dart @@ -679,6 +679,7 @@ class PasswordWidget extends StatefulWidget { this.reRequestFocus = false, this.hintText, this.errorText, + this.title, }) : super(key: key); final TextEditingController controller; @@ -686,6 +687,7 @@ class PasswordWidget extends StatefulWidget { final bool reRequestFocus; final String? hintText; final String? errorText; + final String? title; @override State createState() => _PasswordWidgetState(); @@ -729,7 +731,7 @@ class _PasswordWidgetState extends State { @override Widget build(BuildContext context) { return DialogTextField( - title: translate(DialogTextField.kPasswordTitle), + title: translate(widget.title ?? DialogTextField.kPasswordTitle), hintText: translate(widget.hintText ?? 'Enter your password'), controller: widget.controller, prefixIcon: DialogTextField.kPasswordIcon, @@ -2216,3 +2218,98 @@ void CommonConfirmDialog(OverlayDialogManager dialogManager, String content, ); }); } + +void changeUnlockPinDialog(String oldPin, Function() callback) { + final pinController = TextEditingController(text: oldPin); + final confirmController = TextEditingController(text: oldPin); + String? pinErrorText; + String? confirmationErrorText; + 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, + ), + DialogTextField( + title: translate('Confirmation'), + controller: confirmController, + obscureText: true, + errorText: confirmationErrorText, + ) + ], + ).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, + ); + }); +} diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index ba2c14d6997f..a016a61ae3c0 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -1018,6 +1018,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { _OptionCheckBox(context, 'allow-only-conn-window-open-tip', 'allow-only-conn-window-open', reverse: false, enabled: enabled), + if (bind.mainIsInstalled()) unlockPin() ]); } @@ -1265,6 +1266,40 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { }(), ]; } + + Widget unlockPin() { + bool enabled = !locked; + RxString unlockPin = bind.mainGetUnlockPin().obs; + update() async { + unlockPin.value = bind.mainGetUnlockPin(); + } + + onChanged(bool? checked) async { + changeUnlockPinDialog(unlockPin.value, update); + } + + final isOptFixed = isOptionFixed(kOptionWhitelist); + return GestureDetector( + child: Obx(() => Row( + children: [ + Checkbox( + value: unlockPin.isNotEmpty, + onChanged: enabled && !isOptFixed ? onChanged : null) + .marginOnly(right: 5), + Expanded( + child: Text( + translate('Unlock with PIN'), + style: TextStyle(color: disabledTextColor(context, enabled)), + )) + ], + )), + onTap: enabled + ? () { + onChanged(!unlockPin.isNotEmpty); + } + : null, + ).marginOnly(left: _kCheckBoxLeftMargin); + } } class _Network extends StatefulWidget { @@ -2160,9 +2195,14 @@ Widget _lock( Text(translate(label)).marginOnly(left: 5), ]).marginSymmetric(vertical: 2)), onPressed: () async { - bool checked = await callMainCheckSuperUserPermission(); - if (checked) { - onUnlock(); + final unlockPin = bind.mainGetUnlockPin(); + if (unlockPin.isEmpty) { + bool checked = await callMainCheckSuperUserPermission(); + if (checked) { + onUnlock(); + } + } else { + checkUnlockPinDialog(unlockPin, onUnlock); } }, ).marginSymmetric(horizontal: 2, vertical: 4), diff --git a/flutter/lib/web/bridge.dart b/flutter/lib/web/bridge.dart index d4939c804e60..7795cb82468b 100644 --- a/flutter/lib/web/bridge.dart +++ b/flutter/lib/web/bridge.dart @@ -1622,5 +1622,13 @@ class RustdeskImpl { throw UnimplementedError(); } + String mainGetUnlockPin({dynamic hint}) { + throw UnimplementedError(); + } + + String mainSetUnlockPin({required String pin, dynamic hint}) { + throw UnimplementedError(); + } + void dispose() {} } diff --git a/libs/hbb_common/src/config.rs b/libs/hbb_common/src/config.rs index 0a884e1f8b70..2dfa88b9652b 100644 --- a/libs/hbb_common/src/config.rs +++ b/libs/hbb_common/src/config.rs @@ -208,6 +208,8 @@ pub struct Config2 { nat_type: i32, #[serde(default, deserialize_with = "deserialize_i32")] serial: i32, + #[serde(default, deserialize_with = "deserialize_string")] + unlock_pin: String, #[serde(default)] socks: Option, @@ -427,14 +429,20 @@ fn patch(path: PathBuf) -> PathBuf { impl Config2 { fn load() -> Config2 { let mut config = Config::load_::("2"); + let mut store = false; if let Some(mut socks) = config.socks { - let (password, _, store) = + let (password, _, store2) = decrypt_str_or_original(&socks.password, PASSWORD_ENC_VERSION); socks.password = password; config.socks = Some(socks); - if store { - config.store(); - } + store |= store2; + } + let (unlock_pin, _, store2) = + decrypt_str_or_original(&config.unlock_pin, PASSWORD_ENC_VERSION); + config.unlock_pin = unlock_pin; + store |= store2; + if store { + config.store(); } config } @@ -450,6 +458,8 @@ impl Config2 { encrypt_str_or_original(&socks.password, PASSWORD_ENC_VERSION, ENCRYPT_MAX_LEN); config.socks = Some(socks); } + config.unlock_pin = + encrypt_str_or_original(&config.unlock_pin, PASSWORD_ENC_VERSION, ENCRYPT_MAX_LEN); Config::store_(&config, "2"); } @@ -1081,6 +1091,19 @@ impl Config { NetworkType::Direct } + pub fn get_unlock_pin() -> String { + CONFIG2.read().unwrap().unlock_pin.clone() + } + + pub fn set_unlock_pin(pin: &str) { + let mut config = CONFIG2.write().unwrap(); + if pin == config.unlock_pin { + return; + } + config.unlock_pin = pin.to_string(); + config.store(); + } + pub fn get() -> Config { return CONFIG.read().unwrap().clone(); } diff --git a/src/core_main.rs b/src/core_main.rs index c7e4d1f00885..3aa69f8f367d 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -316,6 +316,20 @@ pub fn core_main() -> Option> { } } return None; + } else if args[0] == "--set-unlock-pin" { + #[cfg(feature = "flutter")] + if args.len() == 2 { + if crate::platform::is_installed() && is_root() { + if let Err(err) = crate::ipc::set_unlock_pin(args[1].to_owned(), false) { + println!("{err}"); + } else { + println!("Done!"); + } + } else { + println!("Installation and administrative privileges required!"); + } + } + return None; } else if args[0] == "--get-id" { println!("{}", crate::ipc::get_id()); return None; diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index cceec4cd1496..46261a37e88d 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -1617,6 +1617,14 @@ pub fn main_check_super_user_permission() -> bool { check_super_user_permission() } +pub fn main_get_unlock_pin() -> SyncReturn { + SyncReturn(get_unlock_pin()) +} + +pub fn main_set_unlock_pin(pin: String) -> SyncReturn { + SyncReturn(set_unlock_pin(pin)) +} + pub fn main_check_mouse_time() { check_mouse_time(); } diff --git a/src/ipc.rs b/src/ipc.rs index fba0000fb6a2..be869488b4f3 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -484,6 +484,8 @@ async fn handle(data: Data, stream: &mut Connection) { }; } else if name == "voice-call-input" { value = crate::audio_service::get_voice_call_input_device(); + } else if name == "unlock-pin" { + value = Some(Config::get_unlock_pin()); } else { value = None; } @@ -501,6 +503,8 @@ async fn handle(data: Data, stream: &mut Connection) { Config::set_salt(&value); } else if name == "voice-call-input" { crate::audio_service::set_voice_call_input_device(Some(value), true); + } else if name == "unlock-pin" { + Config::set_unlock_pin(&value); } else { return; } @@ -891,6 +895,37 @@ pub fn set_permanent_password(v: String) -> ResultType<()> { set_config("permanent-password", v) } +#[cfg(feature = "flutter")] +#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub fn set_unlock_pin(v: String, translate: bool) -> ResultType<()> { + let v = v.trim().to_owned(); + let min_len = 4; + if !v.is_empty() && v.len() < min_len { + let err = if translate { + crate::lang::translate( + "Requires at least {".to_string() + &format!("{min_len}") + "} characters", + ) + } else { + // Sometimes, translated can't show normally in command line + format!("Requires at least {} characters", min_len) + }; + bail!(err); + } + Config::set_unlock_pin(&v); + set_config("unlock-pin", v) +} + +#[cfg(feature = "flutter")] +#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub fn get_unlock_pin() -> String { + if let Ok(Some(v)) = get_config("unlock-pin") { + Config::set_unlock_pin(&v); + v + } else { + Config::get_unlock_pin() + } +} + pub fn get_id() -> String { if let Ok(Some(v)) = get_config("id") { // update salt also, so that next time reinstallation not causing first-time auto-login failure diff --git a/src/lang/ar.rs b/src/lang/ar.rs index 05b8ae546403..d1da83ec576c 100644 --- a/src/lang/ar.rs +++ b/src/lang/ar.rs @@ -632,5 +632,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About RustDesk", ""), ("Send clipboard keystrokes", ""), ("network_error_tip", ""), + ("Unlock with PIN", ""), + ("Requires at least {} characters", ""), + ("Wrong PIN", ""), + ("Set PIN", ""), ].iter().cloned().collect(); } diff --git a/src/lang/be.rs b/src/lang/be.rs index 09936fb5a6ac..a294fcae0cb3 100644 --- a/src/lang/be.rs +++ b/src/lang/be.rs @@ -632,5 +632,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About RustDesk", ""), ("Send clipboard keystrokes", ""), ("network_error_tip", ""), + ("Unlock with PIN", ""), + ("Requires at least {} characters", ""), + ("Wrong PIN", ""), + ("Set PIN", ""), ].iter().cloned().collect(); } diff --git a/src/lang/bg.rs b/src/lang/bg.rs index d4099fa24279..9eb0ca63fd71 100644 --- a/src/lang/bg.rs +++ b/src/lang/bg.rs @@ -632,5 +632,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About RustDesk", ""), ("Send clipboard keystrokes", ""), ("network_error_tip", ""), + ("Unlock with PIN", ""), + ("Requires at least {} characters", ""), + ("Wrong PIN", ""), + ("Set PIN", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ca.rs b/src/lang/ca.rs index 6b3c4c294ac6..5bfea79ca229 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -632,5 +632,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About RustDesk", ""), ("Send clipboard keystrokes", ""), ("network_error_tip", ""), + ("Unlock with PIN", ""), + ("Requires at least {} characters", ""), + ("Wrong PIN", ""), + ("Set PIN", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 48a0875fca73..6481761fdeb7 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -632,5 +632,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About RustDesk", "关于 RustDesk"), ("Send clipboard keystrokes", "发送剪贴板按键"), ("network_error_tip", "请检查网络连接,然后点击再试"), + ("Unlock with PIN", "使用 PIN 码解锁设置"), + ("Requires at least {} characters", "不少于{}个字符"), + ("Wrong PIN", "PIN 码错误"), + ("Set PIN", "设置 PIN 码"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index 4a4cc943c867..2caf00e5d840 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -632,5 +632,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About RustDesk", "O RustDesk"), ("Send clipboard keystrokes", "Odesílat stisky kláves schránky"), ("network_error_tip", "Zkontrolujte prosím připojení k síti a klikněte na tlačítko Opakovat."), + ("Unlock with PIN", ""), + ("Requires at least {} characters", ""), + ("Wrong PIN", ""), + ("Set PIN", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index 5ee83337fa28..44d19dd4600e 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -632,5 +632,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About RustDesk", ""), ("Send clipboard keystrokes", ""), ("network_error_tip", ""), + ("Unlock with PIN", ""), + ("Requires at least {} characters", ""), + ("Wrong PIN", ""), + ("Set PIN", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index 3363d7e59665..fc6269cb7d6f 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -632,5 +632,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About RustDesk", "Über RustDesk"), ("Send clipboard keystrokes", "Tastenanschläge aus der Zwischenablage senden"), ("network_error_tip", "Bitte überprüfen Sie Ihre Netzwerkverbindung und versuchen Sie es dann erneut."), + ("Unlock with PIN", ""), + ("Requires at least {} characters", ""), + ("Wrong PIN", ""), + ("Set PIN", ""), ].iter().cloned().collect(); } diff --git a/src/lang/el.rs b/src/lang/el.rs index c5ed30ebd002..2f929f3b1f8b 100644 --- a/src/lang/el.rs +++ b/src/lang/el.rs @@ -632,5 +632,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About RustDesk", ""), ("Send clipboard keystrokes", ""), ("network_error_tip", ""), + ("Unlock with PIN", ""), + ("Requires at least {} characters", ""), + ("Wrong PIN", ""), + ("Set PIN", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index f92c706c6d22..70d73bfc608b 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -632,5 +632,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About RustDesk", ""), ("Send clipboard keystrokes", ""), ("network_error_tip", ""), + ("Unlock with PIN", ""), + ("Requires at least {} characters", ""), + ("Wrong PIN", ""), + ("Set PIN", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index e5eaadf3bc98..da6c2b4ca47c 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -632,5 +632,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About RustDesk", ""), ("Send clipboard keystrokes", ""), ("network_error_tip", ""), + ("Unlock with PIN", ""), + ("Requires at least {} characters", ""), + ("Wrong PIN", ""), + ("Set PIN", ""), ].iter().cloned().collect(); } diff --git a/src/lang/et.rs b/src/lang/et.rs index bf5742786f87..74c30b076db1 100644 --- a/src/lang/et.rs +++ b/src/lang/et.rs @@ -632,5 +632,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About RustDesk", ""), ("Send clipboard keystrokes", ""), ("network_error_tip", ""), + ("Unlock with PIN", ""), + ("Requires at least {} characters", ""), + ("Wrong PIN", ""), + ("Set PIN", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eu.rs b/src/lang/eu.rs index 487ee23c31a0..55a9dcbfb254 100644 --- a/src/lang/eu.rs +++ b/src/lang/eu.rs @@ -632,5 +632,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About RustDesk", ""), ("Send clipboard keystrokes", ""), ("network_error_tip", ""), + ("Unlock with PIN", ""), + ("Requires at least {} characters", ""), + ("Wrong PIN", ""), + ("Set PIN", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index e7a5af76864f..e9c77bd69237 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -632,5 +632,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About RustDesk", ""), ("Send clipboard keystrokes", ""), ("network_error_tip", ""), + ("Unlock with PIN", ""), + ("Requires at least {} characters", ""), + ("Wrong PIN", ""), + ("Set PIN", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index bf40cd045260..c64aa9ee2575 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -632,5 +632,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About RustDesk", ""), ("Send clipboard keystrokes", ""), ("network_error_tip", ""), + ("Unlock with PIN", ""), + ("Requires at least {} characters", ""), + ("Wrong PIN", ""), + ("Set PIN", ""), ].iter().cloned().collect(); } diff --git a/src/lang/he.rs b/src/lang/he.rs index 84e461c06daa..4e21c204d1a7 100644 --- a/src/lang/he.rs +++ b/src/lang/he.rs @@ -632,5 +632,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About RustDesk", ""), ("Send clipboard keystrokes", ""), ("network_error_tip", ""), + ("Unlock with PIN", ""), + ("Requires at least {} characters", ""), + ("Wrong PIN", ""), + ("Set PIN", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hr.rs b/src/lang/hr.rs index aa08949b2084..f221ebceee9f 100644 --- a/src/lang/hr.rs +++ b/src/lang/hr.rs @@ -632,5 +632,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About RustDesk", ""), ("Send clipboard keystrokes", ""), ("network_error_tip", ""), + ("Unlock with PIN", ""), + ("Requires at least {} characters", ""), + ("Wrong PIN", ""), + ("Set PIN", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 054c333e96a6..40fbf2f768a6 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -632,5 +632,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About RustDesk", ""), ("Send clipboard keystrokes", ""), ("network_error_tip", ""), + ("Unlock with PIN", ""), + ("Requires at least {} characters", ""), + ("Wrong PIN", ""), + ("Set PIN", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index 773a75eab549..eab2e1a36c38 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -632,5 +632,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About RustDesk", ""), ("Send clipboard keystrokes", ""), ("network_error_tip", ""), + ("Unlock with PIN", ""), + ("Requires at least {} characters", ""), + ("Wrong PIN", ""), + ("Set PIN", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index 329db79daa4b..5d152bfcd2b8 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -631,6 +631,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("cancel-bot-confirm-tip", "Sei sicuro di voler annullare Telegram?"), ("About RustDesk", "Info su RustDesk"), ("Send clipboard keystrokes", "Invia sequenze tasti appunti"), - ("network_error_tip", "Controlla la connessione di rete, quindi seleziona 'Riprova'.") + ("network_error_tip", "Controlla la connessione di rete, quindi seleziona 'Riprova'."), + ("Unlock with PIN", ""), + ("Requires at least {} characters", ""), + ("Wrong PIN", ""), + ("Set PIN", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 7bc06d8ccca6..8c8c71944198 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -632,5 +632,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About RustDesk", "RustDeskについて"), ("Send clipboard keystrokes", ""), ("network_error_tip", ""), + ("Unlock with PIN", ""), + ("Requires at least {} characters", ""), + ("Wrong PIN", ""), + ("Set PIN", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 7f7e80d6be12..65db10aecb62 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -632,5 +632,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About RustDesk", "RustDesk 대하여"), ("Send clipboard keystrokes", "클립보드 키 입력 전송"), ("network_error_tip", "네트워크 연결을 확인한 후 다시 시도하세요."), + ("Unlock with PIN", ""), + ("Requires at least {} characters", ""), + ("Wrong PIN", ""), + ("Set PIN", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index 21ef3957499f..7545bff40631 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -632,5 +632,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About RustDesk", ""), ("Send clipboard keystrokes", ""), ("network_error_tip", ""), + ("Unlock with PIN", ""), + ("Requires at least {} characters", ""), + ("Wrong PIN", ""), + ("Set PIN", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lt.rs b/src/lang/lt.rs index 61ff2ee1021e..e659014e59dd 100644 --- a/src/lang/lt.rs +++ b/src/lang/lt.rs @@ -632,5 +632,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About RustDesk", ""), ("Send clipboard keystrokes", ""), ("network_error_tip", ""), + ("Unlock with PIN", ""), + ("Requires at least {} characters", ""), + ("Wrong PIN", ""), + ("Set PIN", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lv.rs b/src/lang/lv.rs index 5ce48429de32..3296a51ae2ef 100644 --- a/src/lang/lv.rs +++ b/src/lang/lv.rs @@ -632,5 +632,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About RustDesk", ""), ("Send clipboard keystrokes", ""), ("network_error_tip", ""), + ("Unlock with PIN", ""), + ("Requires at least {} characters", ""), + ("Wrong PIN", ""), + ("Set PIN", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nb.rs b/src/lang/nb.rs index 3a986ecbb8c4..1afa22d99b39 100644 --- a/src/lang/nb.rs +++ b/src/lang/nb.rs @@ -632,5 +632,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About RustDesk", ""), ("Send clipboard keystrokes", ""), ("network_error_tip", ""), + ("Unlock with PIN", ""), + ("Requires at least {} characters", ""), + ("Wrong PIN", ""), + ("Set PIN", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nl.rs b/src/lang/nl.rs index eceb1b6ff19e..764849991c83 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -632,5 +632,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About RustDesk", "Over RustDesk"), ("Send clipboard keystrokes", "Klembord toetsaanslagen verzenden"), ("network_error_tip", "Controleer de netwerkverbinding en selecteer 'Opnieuw proberen'."), + ("Unlock with PIN", ""), + ("Requires at least {} characters", ""), + ("Wrong PIN", ""), + ("Set PIN", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 370cf59e4e6d..5d04d76c58a4 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -632,5 +632,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About RustDesk", ""), ("Send clipboard keystrokes", ""), ("network_error_tip", ""), + ("Unlock with PIN", ""), + ("Requires at least {} characters", ""), + ("Wrong PIN", ""), + ("Set PIN", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index 0bf0c7304d0f..965d934df9b9 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -632,5 +632,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About RustDesk", ""), ("Send clipboard keystrokes", ""), ("network_error_tip", ""), + ("Unlock with PIN", ""), + ("Requires at least {} characters", ""), + ("Wrong PIN", ""), + ("Set PIN", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index 003786c462b9..b3804bd5c58d 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -632,5 +632,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About RustDesk", ""), ("Send clipboard keystrokes", ""), ("network_error_tip", ""), + ("Unlock with PIN", ""), + ("Requires at least {} characters", ""), + ("Wrong PIN", ""), + ("Set PIN", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ro.rs b/src/lang/ro.rs index bace4107288c..639f0a29c9ff 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -632,5 +632,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About RustDesk", ""), ("Send clipboard keystrokes", ""), ("network_error_tip", ""), + ("Unlock with PIN", ""), + ("Requires at least {} characters", ""), + ("Wrong PIN", ""), + ("Set PIN", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 0c8a24718ee7..ca9e17757d5c 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -632,5 +632,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About RustDesk", "О RustDesk"), ("Send clipboard keystrokes", "Отправлять нажатия клавиш из буфера обмена"), ("network_error_tip", "Проверьте подключение к сети, затем нажмите \"Повтор\"."), + ("Unlock with PIN", ""), + ("Requires at least {} characters", ""), + ("Wrong PIN", ""), + ("Set PIN", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 24b4d2e0998e..816106c57710 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -632,5 +632,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About RustDesk", "O RustDesk"), ("Send clipboard keystrokes", "Odoslať stlačenia klávesov zo schránky"), ("network_error_tip", "Skontrolujte svoje sieťové pripojenie a potom kliknite na tlačidlo Opakovať."), + ("Unlock with PIN", ""), + ("Requires at least {} characters", ""), + ("Wrong PIN", ""), + ("Set PIN", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sl.rs b/src/lang/sl.rs index 98f33956ac3b..1dd8280c6c67 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -632,5 +632,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About RustDesk", ""), ("Send clipboard keystrokes", ""), ("network_error_tip", ""), + ("Unlock with PIN", ""), + ("Requires at least {} characters", ""), + ("Wrong PIN", ""), + ("Set PIN", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sq.rs b/src/lang/sq.rs index c3ebca6290c3..f11d485605e7 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -632,5 +632,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About RustDesk", ""), ("Send clipboard keystrokes", ""), ("network_error_tip", ""), + ("Unlock with PIN", ""), + ("Requires at least {} characters", ""), + ("Wrong PIN", ""), + ("Set PIN", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sr.rs b/src/lang/sr.rs index c6773955af85..2bc72e6e368b 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -632,5 +632,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About RustDesk", ""), ("Send clipboard keystrokes", ""), ("network_error_tip", ""), + ("Unlock with PIN", ""), + ("Requires at least {} characters", ""), + ("Wrong PIN", ""), + ("Set PIN", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sv.rs b/src/lang/sv.rs index 2445bc2f11bc..ed7752e262e5 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -632,5 +632,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About RustDesk", ""), ("Send clipboard keystrokes", ""), ("network_error_tip", ""), + ("Unlock with PIN", ""), + ("Requires at least {} characters", ""), + ("Wrong PIN", ""), + ("Set PIN", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index 3d3c86e5b01d..7e57544a8c6f 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -632,5 +632,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About RustDesk", ""), ("Send clipboard keystrokes", ""), ("network_error_tip", ""), + ("Unlock with PIN", ""), + ("Requires at least {} characters", ""), + ("Wrong PIN", ""), + ("Set PIN", ""), ].iter().cloned().collect(); } diff --git a/src/lang/th.rs b/src/lang/th.rs index fa369336cfd1..6789a45731b4 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -632,5 +632,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About RustDesk", ""), ("Send clipboard keystrokes", ""), ("network_error_tip", ""), + ("Unlock with PIN", ""), + ("Requires at least {} characters", ""), + ("Wrong PIN", ""), + ("Set PIN", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index 366265a57c9f..e9b3d6cf2eb6 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -632,5 +632,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About RustDesk", ""), ("Send clipboard keystrokes", ""), ("network_error_tip", ""), + ("Unlock with PIN", ""), + ("Requires at least {} characters", ""), + ("Wrong PIN", ""), + ("Set PIN", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 8fa1c8c14e71..0a4b26144457 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -632,5 +632,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About RustDesk", "關於 RustDesk"), ("Send clipboard keystrokes", "發送剪貼簿按鍵"), ("network_error_tip", "請檢查網路連結,然後點擊重試"), + ("Unlock with PIN", ""), + ("Requires at least {} characters", ""), + ("Wrong PIN", ""), + ("Set PIN", ""), ].iter().cloned().collect(); } diff --git a/src/lang/uk.rs b/src/lang/uk.rs index c9cee27e4009..aa945877676c 100644 --- a/src/lang/uk.rs +++ b/src/lang/uk.rs @@ -632,5 +632,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About RustDesk", "Про Rustdesk"), ("Send clipboard keystrokes", "Надіслати вміст буфера обміну"), ("network_error_tip", "Будь ласка, перевірте ваше підключення до мережі та натисність \"Повторити\""), + ("Unlock with PIN", ""), + ("Requires at least {} characters", ""), + ("Wrong PIN", ""), + ("Set PIN", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index 6042a98d516d..0953643624f1 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -632,5 +632,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About RustDesk", ""), ("Send clipboard keystrokes", ""), ("network_error_tip", ""), + ("Unlock with PIN", ""), + ("Requires at least {} characters", ""), + ("Wrong PIN", ""), + ("Set PIN", ""), ].iter().cloned().collect(); } diff --git a/src/ui_interface.rs b/src/ui_interface.rs index 443f6fa4276f..e32fcb49d2b7 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -1444,3 +1444,22 @@ pub fn check_hwcodec() { }) } } + +#[cfg(feature = "flutter")] +pub fn get_unlock_pin() -> String { + #[cfg(any(target_os = "android", target_os = "ios"))] + return String::default(); + #[cfg(not(any(target_os = "android", target_os = "ios")))] + return ipc::get_unlock_pin(); +} + +#[cfg(feature = "flutter")] +pub fn set_unlock_pin(pin: String) -> String { + #[cfg(any(target_os = "android", target_os = "ios"))] + return String::default(); + #[cfg(not(any(target_os = "android", target_os = "ios")))] + match ipc::set_unlock_pin(pin, true) { + Ok(_) => String::default(), + Err(err) => err.to_string(), + } +} From 9a194f085060c171009430cfd78d0a784b2dc656 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Wed, 7 Aug 2024 23:26:03 +0800 Subject: [PATCH 068/541] res/users.py --- res/devices.py | 2 + res/users.py | 123 +++++++++++++++++++++++++++++++++++++++++++++++++ src/tray.rs | 2 +- 3 files changed, 126 insertions(+), 1 deletion(-) create mode 100755 res/users.py diff --git a/res/devices.py b/res/devices.py index d72614b70465..f9bf27352921 100755 --- a/res/devices.py +++ b/res/devices.py @@ -128,6 +128,8 @@ def main(): ) args = parser.parse_args() + + while args.url.endswith("/"): args.url = args.url[:-1] devices = view( args.url, diff --git a/res/users.py b/res/users.py new file mode 100755 index 000000000000..54297f06ae82 --- /dev/null +++ b/res/users.py @@ -0,0 +1,123 @@ +#!/usr/bin/env python3 + +import requests +import argparse +from datetime import datetime, timedelta + + +def view( + url, + token, + name=None, + group_name=None, +): + headers = {"Authorization": f"Bearer {token}"} + pageSize = 30 + params = { + "name": name, + "group_name": group_name, + } + + params = { + k: "%" + v + "%" if (v != "-" and "%" not in v) else v + for k, v in params.items() + if v is not None + } + params["pageSize"] = pageSize + + users = [] + + current = 1 + + while True: + params["current"] = current + response = requests.get(f"{url}/api/users", headers=headers, params=params) + response_json = response.json() + + data = response_json.get("data", []) + users.extend(data) + + total = response_json.get("total", 0) + current += pageSize + if len(data) < pageSize or current > total: + break + + return users + + +def check(response): + if response.status_code == 200: + try: + response_json = response.json() + return response_json + except ValueError: + return response.text or "Success" + else: + return "Failed", response.status_code, response.text + + +def disable(url, token, guid, name): + print("Disable", name) + headers = {"Authorization": f"Bearer {token}"} + response = requests.post(f"{url}/api/users/{guid}/disable", headers=headers) + return check(response) + + +def enable(url, token, guid, name): + print("Enable", name) + headers = {"Authorization": f"Bearer {token}"} + response = requests.post(f"{url}/api/users/{guid}/enable", headers=headers) + return check(response) + + +def delete(url, token, guid, name): + print("Delete", name) + headers = {"Authorization": f"Bearer {token}"} + response = requests.delete(f"{url}/api/users/{guid}", headers=headers) + return check(response) + + +def main(): + parser = argparse.ArgumentParser(description="User manager") + parser.add_argument( + "command", + choices=["view", "disable", "enable", "delete"], + help="Command to execute", + ) + parser.add_argument("--url", required=True, help="URL of the API") + parser.add_argument( + "--token", required=True, help="Bearer token for authentication" + ) + parser.add_argument("--name", help="User name") + parser.add_argument("--group_name", help="Group name") + + args = parser.parse_args() + + while args.url.endswith("/"): args.url = args.url[:-1] + + users = view( + args.url, + args.token, + args.name, + args.group_name, + ) + + if args.command == "view": + for user in users: + print(user) + elif args.command == "disable": + for user in users: + response = disable(args.url, args.token, user["guid"], user["name"]) + print(response) + elif args.command == "enable": + for user in users: + response = enable(args.url, args.token, user["guid"], user["name"]) + print(response) + elif args.command == "delete": + for user in users: + response = delete(args.url, args.token, user["guid"], user["name"]) + print(response) + + +if __name__ == "__main__": + main() diff --git a/src/tray.rs b/src/tray.rs index 39d650662c78..3a879801e486 100644 --- a/src/tray.rs +++ b/src/tray.rs @@ -25,7 +25,7 @@ pub fn start_tray() { allow_err!(make_tray()); } -pub fn make_tray() -> hbb_common::ResultType<()> { +fn make_tray() -> hbb_common::ResultType<()> { // https://github.com/tauri-apps/tray-icon/blob/dev/examples/tao.rs use hbb_common::anyhow::Context; use tao::event_loop::{ControlFlow, EventLoopBuilder}; From ade1d8c0c72c86c22195c5ab489ac5144167b896 Mon Sep 17 00:00:00 2001 From: bovirus <1262554+bovirus@users.noreply.github.com> Date: Thu, 8 Aug 2024 04:24:15 +0200 Subject: [PATCH 069/541] Update Italian language (#8988) --- src/lang/it.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/lang/it.rs b/src/lang/it.rs index 5d152bfcd2b8..c992d033a246 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -632,9 +632,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About RustDesk", "Info su RustDesk"), ("Send clipboard keystrokes", "Invia sequenze tasti appunti"), ("network_error_tip", "Controlla la connessione di rete, quindi seleziona 'Riprova'."), - ("Unlock with PIN", ""), - ("Requires at least {} characters", ""), - ("Wrong PIN", ""), - ("Set PIN", ""), + ("Unlock with PIN", "Sblocca con PIN"), + ("Requires at least {} characters", "Richiede almeno {} caratteri"), + ("Wrong PIN", "PIN errato"), + ("Set PIN", "Imposta PIN"), ].iter().cloned().collect(); } From 171177c76f34b13aded26ab6e1976b3572374344 Mon Sep 17 00:00:00 2001 From: solokot Date: Thu, 8 Aug 2024 05:24:28 +0300 Subject: [PATCH 070/541] Update ru.rs (#8987) --- src/lang/ru.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/lang/ru.rs b/src/lang/ru.rs index ca9e17757d5c..f3fe46f3a292 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -632,9 +632,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About RustDesk", "О RustDesk"), ("Send clipboard keystrokes", "Отправлять нажатия клавиш из буфера обмена"), ("network_error_tip", "Проверьте подключение к сети, затем нажмите \"Повтор\"."), - ("Unlock with PIN", ""), - ("Requires at least {} characters", ""), - ("Wrong PIN", ""), - ("Set PIN", ""), + ("Unlock with PIN", "Разблокировать PIN-кодом"), + ("Requires at least {} characters", "Требуется не менее {} символов"), + ("Wrong PIN", "Неправильный PIN-код"), + ("Set PIN", "Установить PIN-код"), ].iter().cloned().collect(); } From ae16b8975bdb66c7e74c600797fd98826f107eb7 Mon Sep 17 00:00:00 2001 From: Yevhen Popok Date: Thu, 8 Aug 2024 05:24:40 +0300 Subject: [PATCH 071/541] Update Ukrainian translation (#8980) Sync with #8977 --- src/lang/uk.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/lang/uk.rs b/src/lang/uk.rs index aa945877676c..3d031a65e791 100644 --- a/src/lang/uk.rs +++ b/src/lang/uk.rs @@ -632,9 +632,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About RustDesk", "Про Rustdesk"), ("Send clipboard keystrokes", "Надіслати вміст буфера обміну"), ("network_error_tip", "Будь ласка, перевірте ваше підключення до мережі та натисність \"Повторити\""), - ("Unlock with PIN", ""), - ("Requires at least {} characters", ""), - ("Wrong PIN", ""), - ("Set PIN", ""), + ("Unlock with PIN", "Розблокування PIN-кодом"), + ("Requires at least {} characters", "Потрібно щонайменше {} символів"), + ("Wrong PIN", "Неправильний PIN-код"), + ("Set PIN", "Встановити PIN-код"), ].iter().cloned().collect(); } From 2fd53f9825693d5f73f9d5480ffc931f3fc3ef42 Mon Sep 17 00:00:00 2001 From: Vasyl Gello Date: Thu, 8 Aug 2024 03:53:49 +0000 Subject: [PATCH 072/541] Pass JVM to ffmpeg (#8985) Signed-off-by: Vasyl Gello --- libs/scrap/src/android/ffi.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/libs/scrap/src/android/ffi.rs b/libs/scrap/src/android/ffi.rs index d77dcce98964..f5208a673b27 100644 --- a/libs/scrap/src/android/ffi.rs +++ b/libs/scrap/src/android/ffi.rs @@ -349,6 +349,10 @@ fn init_ndk_context() -> JniResult<()> { jvm.get_java_vm_pointer() as _, ctx.as_obj().as_raw() as _, ); + #[cfg(feature = "hwcodec")] + hwcodec::android::ffmpeg_set_java_vm( + jvm.get_java_vm_pointer() as _, + ); } *lock = true; return Ok(()); From 619783231796d0a12fb0d3255da406d443de7132 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Thu, 8 Aug 2024 11:58:12 +0800 Subject: [PATCH 073/541] update hwcodec --- Cargo.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index b5d653579484..ef2cab92c807 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3045,7 +3045,7 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hwcodec" version = "0.7.0" -source = "git+https://github.com/rustdesk-org/hwcodec#97522dfcd10b46af4c2ac1a66b769d69f8fd582a" +source = "git+https://github.com/rustdesk-org/hwcodec#6abd1898f3a03481ed0c038507b5218d6ea94267" dependencies = [ "bindgen 0.59.2", "cc", From 049c334db325cfe3ed9d4d9eb3dc17d3ea7214cd Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Thu, 8 Aug 2024 15:31:11 +0800 Subject: [PATCH 074/541] fix: privacy mode, windows vd, init resolution (#8992) Signed-off-by: fufesou --- src/privacy_mode.rs | 3 +++ src/privacy_mode/win_topmost_window.rs | 2 +- src/privacy_mode/win_virtual_display.rs | 12 +++++++++++- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/privacy_mode.rs b/src/privacy_mode.rs index 7116095f1e1b..8b1510625ca5 100644 --- a/src/privacy_mode.rs +++ b/src/privacy_mode.rs @@ -187,6 +187,7 @@ fn get_supported_impl(impl_key: &str) -> String { if supported_impls.iter().any(|(k, _)| k == &impl_key) { return impl_key.to_owned(); }; + // TODO: Is it a good idea to use fallback here? Because user do not know the fallback. // fallback let mut cur_impl = get_option("privacy-mode-impl-key".to_owned()); if !get_supported_privacy_mode_impl() @@ -223,6 +224,8 @@ async fn turn_on_privacy_async(impl_key: String, conn_id: i32) -> Option match res { Ok(res) => res, diff --git a/src/privacy_mode/win_topmost_window.rs b/src/privacy_mode/win_topmost_window.rs index fdfcfcba6e17..a7f80a02d726 100644 --- a/src/privacy_mode/win_topmost_window.rs +++ b/src/privacy_mode/win_topmost_window.rs @@ -75,7 +75,7 @@ impl PrivacyMode for PrivacyModeImpl { fn is_async_privacy_mode(&self) -> bool { false } - + fn init(&self) -> ResultType<()> { Ok(()) } diff --git a/src/privacy_mode/win_virtual_display.rs b/src/privacy_mode/win_virtual_display.rs index 0bf4b721f480..69fff6310039 100644 --- a/src/privacy_mode/win_virtual_display.rs +++ b/src/privacy_mode/win_virtual_display.rs @@ -213,6 +213,8 @@ impl PrivacyModeImpl { dm.u1.s2_mut().dmPosition.x -= new_primary_dm.u1.s2().dmPosition.x; dm.u1.s2_mut().dmPosition.y -= new_primary_dm.u1.s2().dmPosition.y; dm.dmFields |= DM_POSITION; + dm.dmPelsWidth = 1920; + dm.dmPelsHeight = 1080; let rc = ChangeDisplaySettingsExW( dd.DeviceName.as_ptr(), &mut dm, @@ -220,7 +222,6 @@ impl PrivacyModeImpl { flags, NULL, ); - if rc != DISP_CHANGE_SUCCESSFUL { log::error!( "Failed ChangeDisplaySettingsEx, device name: {:?}, flags: {}, ret: {}", @@ -230,6 +231,15 @@ impl PrivacyModeImpl { ); bail!("Failed ChangeDisplaySettingsEx, ret: {}", rc); } + + // If we want to set dpi, the following references may be helpful. + // And setting dpi should be called after changing the display settings. + // https://stackoverflow.com/questions/35233182/how-can-i-change-windows-10-display-scaling-programmatically-using-c-sharp + // https://github.com/lihas/windows-DPI-scaling-sample/blob/master/DPIHelper/DpiHelper.cpp + // + // But the official API does not provide a way to get/set dpi. + // https://learn.microsoft.com/en-us/windows/win32/api/wingdi/ne-wingdi-displayconfig_device_info_type + // https://github.com/lihas/windows-DPI-scaling-sample/blob/738ac18b7a7ce2d8fdc157eb825de9cb5eee0448/DPIHelper/DpiHelper.h#L37 } } From f4c40d733e74879c122155fe4980798b2f3a0523 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Thu, 8 Aug 2024 22:07:06 +0800 Subject: [PATCH 075/541] Fix/exe upgrade options (#9001) * fix: exe upgrade, use previous options Signed-off-by: fufesou * refact: msi, shortcuts options, swap pos Signed-off-by: fufesou --------- Signed-off-by: fufesou --- flutter/lib/desktop/pages/install_page.dart | 6 ++ res/msi/Package/UI/MyInstallDirDlg.wxs | 4 +- src/flutter_ffi.rs | 4 ++ src/platform/windows.rs | 73 ++++++++++++++++++--- src/ui.rs | 5 ++ src/ui/install.tis | 7 +- src/ui_interface.rs | 8 +++ 7 files changed, 94 insertions(+), 13 deletions(-) diff --git a/flutter/lib/desktop/pages/install_page.dart b/flutter/lib/desktop/pages/install_page.dart index a860fe89ea1c..5285fc35f2bf 100644 --- a/flutter/lib/desktop/pages/install_page.dart +++ b/flutter/lib/desktop/pages/install_page.dart @@ -1,3 +1,5 @@ +import 'dart:convert'; + import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/common.dart'; @@ -73,6 +75,9 @@ class _InstallPageBodyState extends State<_InstallPageBody> _InstallPageBodyState() { controller = TextEditingController(text: bind.installInstallPath()); + final installOptions = jsonDecode(bind.installInstallOptions()); + startmenu.value = installOptions['STARTMENUSHORTCUTS'] != '0'; + desktopicon.value = installOptions['DESKTOPSHORTCUTS'] != '0'; } @override @@ -249,6 +254,7 @@ class _InstallPageBodyState extends State<_InstallPageBody> if (desktopicon.value) args += ' desktopicon'; bind.installInstallMe(options: args, path: controller.text); } + do_install(); } diff --git a/res/msi/Package/UI/MyInstallDirDlg.wxs b/res/msi/Package/UI/MyInstallDirDlg.wxs index 7c554a1215de..6e27e2b28268 100644 --- a/res/msi/Package/UI/MyInstallDirDlg.wxs +++ b/res/msi/Package/UI/MyInstallDirDlg.wxs @@ -23,8 +23,8 @@ - - + + diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 46261a37e88d..7fb324b3f4f0 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -1849,6 +1849,10 @@ pub fn install_install_path() -> SyncReturn { SyncReturn(install_path()) } +pub fn install_install_options() -> SyncReturn { + SyncReturn(install_options()) +} + pub fn main_account_auth(op: String, remember_me: bool) { let id = get_id(); let uuid = get_uuid(); diff --git a/src/platform/windows.rs b/src/platform/windows.rs index 09ff347a295c..51aee97a3799 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -1,21 +1,20 @@ use super::{CursorData, ResultType}; -use crate::common::PORTABLE_APPNAME_RUNTIME_ENV_KEY; use crate::{ + common::PORTABLE_APPNAME_RUNTIME_ENV_KEY, custom_server::*, ipc, privacy_mode::win_topmost_window::{self, WIN_TOPMOST_INJECTED_PROCESS_EXE}, }; -use hbb_common::libc::{c_int, wchar_t}; use hbb_common::{ allow_err, anyhow::anyhow, bail, config::{self, Config}, + libc::{c_int, wchar_t}, log, message_proto::{DisplayInfo, Resolution, WindowsSession}, sleep, timeout, tokio, }; -use std::process::{Command, Stdio}; use std::{ collections::HashMap, ffi::{CString, OsString}, @@ -24,15 +23,16 @@ use std::{ mem, os::windows::process::CommandExt, path::*, + process::{Command, Stdio}, ptr::null_mut, sync::{atomic::Ordering, Arc, Mutex}, time::{Duration, Instant}, }; use wallpaper; -use winapi::um::sysinfoapi::{GetNativeSystemInfo, SYSTEM_INFO}; use winapi::{ ctypes::c_void, shared::{minwindef::*, ntdef::NULL, windef::*, winerror::*}, + um::sysinfoapi::{GetNativeSystemInfo, SYSTEM_INFO}, um::{ errhandlingapi::GetLastError, handleapi::CloseHandle, @@ -63,13 +63,15 @@ use windows_service::{ }, service_control_handler::{self, ServiceControlHandlerResult}, }; -use winreg::enums::*; -use winreg::RegKey; +use winreg::{enums::*, RegKey}; pub const FLUTTER_RUNNER_WIN32_WINDOW_CLASS: &'static str = "FLUTTER_RUNNER_WIN32_WINDOW"; // main window, install window pub const EXPLORER_EXE: &'static str = "explorer.exe"; pub const SET_FOREGROUND_WINDOW: &'static str = "SET_FOREGROUND_WINDOW"; +const REG_NAME_INSTALL_DESKTOPSHORTCUTS: &str = "DESKTOPSHORTCUTS"; +const REG_NAME_INSTALL_STARTMENUSHORTCUTS: &str = "STARTMENUSHORTCUTS"; + pub fn get_focused_display(displays: Vec) -> Option { unsafe { let hwnd = GetForegroundWindow(); @@ -992,6 +994,32 @@ fn get_valid_subkey() -> String { return get_subkey(&app_name, false); } +// Return install options other than InstallLocation. +pub fn get_install_options() -> String { + let app_name = crate::get_app_name(); + let subkey = format!(".{}", app_name.to_lowercase()); + let mut opts = HashMap::new(); + + let desktop_shortcuts = get_reg_of_hkcr(&subkey, REG_NAME_INSTALL_DESKTOPSHORTCUTS); + if let Some(desktop_shortcuts) = desktop_shortcuts { + opts.insert(REG_NAME_INSTALL_DESKTOPSHORTCUTS, desktop_shortcuts); + } + let start_menu_shortcuts = get_reg_of_hkcr(&subkey, REG_NAME_INSTALL_STARTMENUSHORTCUTS); + if let Some(start_menu_shortcuts) = start_menu_shortcuts { + opts.insert(REG_NAME_INSTALL_STARTMENUSHORTCUTS, start_menu_shortcuts); + } + serde_json::to_string(&opts).unwrap_or("{}".to_owned()) +} + +// This function return Option, because some registry value may be empty. +fn get_reg_of_hkcr(subkey: &str, name: &str) -> Option { + let hkcr = RegKey::predef(HKEY_CLASSES_ROOT); + if let Ok(tmp) = hkcr.open_subkey(subkey.replace("HKEY_CLASSES_ROOT\\", "")) { + return tmp.get_value(name).ok(); + } + None +} + pub fn get_install_info() -> (String, String, String, String) { get_install_info_with_subkey(get_valid_subkey()) } @@ -1101,7 +1129,11 @@ pub fn copy_exe_cmd(src_exe: &str, exe: &str, path: &str) -> ResultType )) } -fn get_after_install(exe: &str) -> String { +fn get_after_install( + exe: &str, + reg_value_start_menu_shortcuts: Option, + reg_value_desktop_shortcuts: Option, +) -> String { let app_name = crate::get_app_name(); let ext = app_name.to_lowercase(); @@ -1112,9 +1144,24 @@ fn get_after_install(exe: &str) -> String { hcu.delete_subkey_all(format!("Software\\Classes\\{}", exe)) .ok(); + let desktop_shortcuts = reg_value_desktop_shortcuts + .map(|v| { + format!("reg add HKEY_CLASSES_ROOT\\.{ext} /f /v {REG_NAME_INSTALL_DESKTOPSHORTCUTS} /t REG_SZ /d \"{v}\"") + }) + .unwrap_or_default(); + let start_menu_shortcuts = reg_value_start_menu_shortcuts + .map(|v| { + format!( + "reg add HKEY_CLASSES_ROOT\\.{ext} /f /v {REG_NAME_INSTALL_STARTMENUSHORTCUTS} /t REG_SZ /d \"{v}\"" + ) + }) + .unwrap_or_default(); + format!(" chcp 65001 reg add HKEY_CLASSES_ROOT\\.{ext} /f + {desktop_shortcuts} + {start_menu_shortcuts} reg add HKEY_CLASSES_ROOT\\.{ext}\\DefaultIcon /f reg add HKEY_CLASSES_ROOT\\.{ext}\\DefaultIcon /f /ve /t REG_SZ /d \"\\\"{exe}\\\",0\" reg add HKEY_CLASSES_ROOT\\.{ext}\\shell /f @@ -1197,6 +1244,8 @@ oLink.Save .unwrap_or("") .to_owned(); let tray_shortcut = get_tray_shortcut(&exe, &tmp_path)?; + let mut reg_value_desktop_shortcuts = "0".to_owned(); + let mut reg_value_start_menu_shortcuts = "0".to_owned(); let mut shortcuts = Default::default(); if options.contains("desktopicon") { shortcuts = format!( @@ -1204,6 +1253,7 @@ oLink.Save tmp_path, crate::get_app_name() ); + reg_value_desktop_shortcuts = "1".to_owned(); } if options.contains("startmenu") { shortcuts = format!( @@ -1213,6 +1263,7 @@ copy /Y \"{tmp_path}\\{app_name}.lnk\" \"{start_menu}\\\" copy /Y \"{tmp_path}\\Uninstall {app_name}.lnk\" \"{start_menu}\\\" " ); + reg_value_start_menu_shortcuts = "1".to_owned(); } let meta = std::fs::symlink_metadata(std::env::current_exe()?)?; @@ -1281,7 +1332,11 @@ copy /Y \"{tmp_path}\\Uninstall {app_name}.lnk\" \"{path}\\\" ", version = crate::VERSION.replace("-", "."), build_date = crate::BUILD_DATE, - after_install = get_after_install(&exe), + after_install = get_after_install( + &exe, + Some(reg_value_start_menu_shortcuts), + Some(reg_value_desktop_shortcuts) + ), sleep = if debug { "timeout 300" } else { "" }, dels = if debug { "" } else { &dels }, copy_exe = copy_exe_cmd(&src_exe, &exe, &path)?, @@ -1294,7 +1349,7 @@ copy /Y \"{tmp_path}\\Uninstall {app_name}.lnk\" \"{path}\\\" pub fn run_after_install() -> ResultType<()> { let (_, _, _, exe) = get_install_info(); - run_cmds(get_after_install(&exe), true, "after_install") + run_cmds(get_after_install(&exe, None, None), true, "after_install") } pub fn run_before_uninstall() -> ResultType<()> { diff --git a/src/ui.rs b/src/ui.rs index aa36fc578ea0..d3d291433ba8 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -312,6 +312,10 @@ impl UI { install_path() } + fn install_options(&self) -> String { + install_options() + } + fn get_socks(&self) -> Value { Value::from_iter(get_socks()) } @@ -683,6 +687,7 @@ impl sciter::EventHandler for UI { fn set_share_rdp(bool); fn is_installed_lower_version(); fn install_path(); + fn install_options(); fn goto_install(); fn is_process_trusted(bool); fn is_can_screen_recording(bool); diff --git a/src/ui/install.tis b/src/ui/install.tis index 3a7920bcfdea..fad4071233a6 100644 --- a/src/ui/install.tis +++ b/src/ui/install.tis @@ -6,13 +6,16 @@ var install_path = ""; class Install: Reactor.Component { function render() { + const install_options = JSON.parse(view.install_options()); + const desktop_icon = { checked: install_options?.DESKTOPSHORTCUTS == '0' ? false : true }; + const startmenu_shortcuts = { checked: install_options?.STARTMENUSHORTCUTS == '0' ? false : true }; return
{translate('Installation')}
{translate('Installation Path')} {": "}
-
{translate('Create start menu shortcuts')}
-
{translate('Create desktop icon')}
+
{translate('Create start menu shortcuts')}
+
{translate('Create desktop icon')}
{translate('End-user license agreement')}
{translate('agreement_tip')}
diff --git a/src/ui_interface.rs b/src/ui_interface.rs index e32fcb49d2b7..c39a58068880 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -426,6 +426,14 @@ pub fn install_path() -> String { return "".to_owned(); } +#[inline] +pub fn install_options() -> String { + #[cfg(windows)] + return crate::platform::windows::get_install_options(); + #[cfg(not(windows))] + return "{}".to_owned(); +} + #[inline] pub fn get_socks() -> Vec { #[cfg(not(any(target_os = "android", target_os = "ios")))] From e7e244d4f25bb7ffdcfb4add9c8addabce27c10a Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Thu, 8 Aug 2024 23:06:33 +0800 Subject: [PATCH 076/541] fix: update pub desktop_multi_window (#9002) Signed-off-by: fufesou --- flutter/pubspec.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index 72eaeabc22a5..7b3b77d6d1fe 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -335,7 +335,7 @@ packages: description: path: "." ref: HEAD - resolved-ref: "336308d86ec8b9640504a371b50ba500eb779363" + resolved-ref: "0d4606f95b3926566aeacab16c6db1cb9ce3d3fa" url: "https://github.com/rustdesk-org/rustdesk_desktop_multi_window" source: git version: "0.1.0" From 77f3ebaf1a846c56ecde7e9e318c7cb968751f16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Be=C3=A0?= Date: Thu, 8 Aug 2024 17:18:01 +0200 Subject: [PATCH 077/541] Update ca.rs (#8997) * Update ca.rs update catalan translation * Update ca.rs minor changes --- src/lang/ca.rs | 436 ++++++++++++++++++++++++------------------------- 1 file changed, 218 insertions(+), 218 deletions(-) diff --git a/src/lang/ca.rs b/src/lang/ca.rs index 5bfea79ca229..4721ed19cd7b 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -8,8 +8,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Ready", "Llest"), ("Established", "Establert"), ("connecting_status", "Connexió a la xarxa RustDesk en progrés..."), - ("Enable service", "Habilitar Servei"), - ("Start service", "Iniciar Servei"), + ("Enable service", "Habilita el servei"), + ("Start service", "Inicia el servei"), ("Service is running", "El servei s'està executant"), ("Service is not running", "El servei no s'està executant"), ("not_ready_status", "No està llest. Comprova la teva connexió"), @@ -21,32 +21,32 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Confirmation", "Confirmació"), ("TCP tunneling", "Túnel TCP"), ("Remove", "Eliminar"), - ("Refresh random password", "Actualitzar contrasenya aleatòria"), - ("Set your own password", "Estableix la teva pròpia contrasenya"), - ("Enable keyboard/mouse", "Habilitar teclat/ratolí"), - ("Enable clipboard", "Habilitar portapapers"), - ("Enable file transfer", "Habilitar transferència d'arxius"), - ("Enable TCP tunneling", "Habilitar túnel TCP"), + ("Refresh random password", "Actualitza la contrasenya aleatòria"), + ("Set your own password", "Estableix la teva contrasenya"), + ("Enable keyboard/mouse", "Habilita el teclat/ratolí"), + ("Enable clipboard", "Habilita el portapapers"), + ("Enable file transfer", "Habilita la transferència d'arxius"), + ("Enable TCP tunneling", "Habilita el túnel TCP"), ("IP Whitelisting", "Adreces IP admeses"), ("ID/Relay Server", "Servidor ID/Relay"), - ("Import server config", "Importar configuració de servidor"), - ("Export Server Config", "Exportar configuració del servidor"), - ("Import server configuration successfully", "Configuració de servidor importada amb èxit"), - ("Export server configuration successfully", "Configuració de servidor exportada con èxit"), - ("Invalid server configuration", "Configuració de servidor incorrecta"), + ("Import server config", "Importa la configuració del servidor"), + ("Export Server Config", "Exporta la configuració del servidor"), + ("Import server configuration successfully", "Configuració del servidor importada amb èxit"), + ("Export server configuration successfully", "Configuració del servidor exportada con èxit"), + ("Invalid server configuration", "Configuració del servidor incorrecta"), ("Clipboard is empty", "El portapapers està buit"), - ("Stop service", "Aturar servei"), + ("Stop service", "Atura el servei"), ("Change ID", "Canviar ID"), ("Your new ID", "La teva nova ID"), ("length %min% to %max%", "Ha de tenir entre %min% i %max% caràcters"), - ("starts with a letter", "començar amb una lletra"), + ("starts with a letter", "comença amb una lletra"), ("allowed characters", "caràcters permesos"), ("id_change_tip", "Només pots utilitzar caràcters a-z, A-Z, 0-9 e _ (guionet baix). El primer caràcter ha de ser a-z o A-Z. La longitut ha d'estar entre 6 i 16 caràcters."), ("Website", "Lloc web"), ("About", "Sobre"), ("Slogan_tip", ""), ("Privacy Statement", "Declaració de privacitat"), - ("Mute", "Silenciar"), + ("Mute", "Silencia"), ("Build Date", "Data de creació"), ("Version", "Versió"), ("Home", "Inici"), @@ -63,79 +63,79 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("server_not_support", "Encara no és compatible amb el servidor"), ("Not available", "No disponible"), ("Too frequent", "Massa comú"), - ("Cancel", "Cancel·lar"), - ("Skip", "Saltar"), - ("Close", "Tancar"), - ("Retry", "Reintentar"), + ("Cancel", "Cancel·la"), + ("Skip", "Salta"), + ("Close", "Tanca"), + ("Retry", "Reintenta"), ("OK", "D'acord"), ("Password Required", "Es necessita la contrasenya"), - ("Please enter your password", "Si us plau, introdueixi la seva contrasenya"), - ("Remember password", "Recordar contrasenya"), + ("Please enter your password", "Introdueix la teva contrasenya"), + ("Remember password", "Recorda la contrasenya"), ("Wrong Password", "Contrasenya incorrecta"), - ("Do you want to enter again?", "Vol tornar a entrar?"), + ("Do you want to enter again?", "Vols tornar a entrar?"), ("Connection Error", "Error de connexió"), ("Error", "Error"), ("Reset by the peer", "Reestablert pel peer"), ("Connecting...", "Connectant..."), - ("Connection in progress. Please wait.", "Connexió en procés. Esperi."), - ("Please try 1 minute later", "Torni a provar-ho d'aquí un minut"), - ("Login Error", "Error d'inicio de sessió"), + ("Connection in progress. Please wait.", "Connexió en procés. Espera."), + ("Please try 1 minute later", "Torna a provar-ho d'aquí un minut"), + ("Login Error", "Error d'inici de sessió"), ("Successful", "Exitós"), ("Connected, waiting for image...", "Connectant, esperant imatge..."), ("Name", "Nom"), ("Type", "Tipus"), ("Modified", "Modificat"), ("Size", "Grandària"), - ("Show Hidden Files", "Mostrar arxius ocults"), - ("Receive", "Rebre"), - ("Send", "Enviar"), - ("Refresh File", "Actualitzar arxiu"), + ("Show Hidden Files", "Mostra arxius ocults"), + ("Receive", "Rep"), + ("Send", "Envia"), + ("Refresh File", "Actualitza el fitxer"), ("Local", "Local"), ("Remote", "Remot"), ("Remote Computer", "Ordinador remot"), ("Local Computer", "Ordinador local"), - ("Confirm Delete", "Confirma eliminació"), - ("Delete", "Eliminar"), + ("Confirm Delete", "Confirma l'eliminació"), + ("Delete", "Elimina"), ("Properties", "Propietats"), ("Multi Select", "Selecció múltiple"), - ("Select All", "Selecciona-ho Tot"), - ("Unselect All", "Deselecciona-ho Tot"), - ("Empty Directory", "Directori buit"), - ("Not an empty directory", "No és un directori buit"), - ("Are you sure you want to delete this file?", "Estàs segur que vols eliminar aquest arxiu?"), - ("Are you sure you want to delete this empty directory?", "Estàs segur que vols eliminar aquest directori buit?"), - ("Are you sure you want to delete the file of this directory?", "Estàs segur que vols eliminar aquest arxiu d'aquest directori?"), + ("Select All", "Selecciona-ho tot"), + ("Unselect All", "Deselecciona-ho tot"), + ("Empty Directory", "Carpeta buida"), + ("Not an empty directory", "No és una carpeta buida"), + ("Are you sure you want to delete this file?", "Segur que vols eliminar aquest fitxer?"), + ("Are you sure you want to delete this empty directory?", "Segur que vols eliminar aquesta carpeta buida?"), + ("Are you sure you want to delete the file of this directory?", "Segur que vols eliminar aquest fitxer d'aquesta car`peta?"), ("Do this for all conflicts", "Fes això per a tots els conflictes"), ("This is irreversible!", "Això és irreversible!"), ("Deleting", "Eliminant"), - ("files", "arxius"), + ("files", "fitxers"), ("Waiting", "Esperant"), ("Finished", "Acabat"), ("Speed", "Velocitat"), ("Custom Image Quality", "Qualitat d'imatge personalitzada"), ("Privacy mode", "Mode privat"), - ("Block user input", "Bloquejar entrada d'usuari"), - ("Unblock user input", "Desbloquejar entrada d'usuari"), - ("Adjust Window", "Ajustar finestra"), + ("Block user input", "Bloqueja l'entrada d'usuari"), + ("Unblock user input", "Desbloqueja l'entrada d'usuari"), + ("Adjust Window", "Ajusta la finestra"), ("Original", "Original"), ("Shrink", "Reduir"), ("Stretch", "Estirar"), ("Scrollbar", "Barra de desplaçament"), - ("ScrollAuto", "Desplaçament automàtico"), + ("ScrollAuto", "Desplaçament automàtic"), ("Good image quality", "Bona qualitat d'imatge"), ("Balanced", "Equilibrat"), - ("Optimize reaction time", "Optimitzar el temps de reacció"), + ("Optimize reaction time", "Optimitza el temps de reacció"), ("Custom", "Personalitzat"), - ("Show remote cursor", "Mostrar cursor remot"), - ("Show quality monitor", "Mostrar qualitat del monitor"), - ("Disable clipboard", "Deshabilitar portapapers"), - ("Lock after session end", "Bloquejar després del final de la sessió"), - ("Insert", "Inserir"), - ("Insert Lock", "Inserir bloqueig"), - ("Refresh", "Actualitzar"), + ("Show remote cursor", "Mostra el cursor remot"), + ("Show quality monitor", "Mostra la qualitat del monitor"), + ("Disable clipboard", "Deshabilita el portapapers"), + ("Lock after session end", "Bloqueja després del final de la sessió"), + ("Insert", "Insereix"), + ("Insert Lock", "Insereix bloqueig"), + ("Refresh", "Actualitza"), ("ID does not exist", "L'ID no existeix"), ("Failed to connect to rendezvous server", "No es pot connectar al servidor rendezvous"), - ("Please try later", "Siusplau provi-ho més tard"), + ("Please try later", "Prova-ho més tard"), ("Remote desktop is offline", "L'escriptori remot està desconecctat"), ("Key mismatch", "La clau no coincideix"), ("Timeout", "Temps esgotat"), @@ -143,60 +143,60 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Failed to connect via rendezvous server", "No es pot connectar a través del servidor de rendezvous"), ("Failed to connect via relay server", "No es pot connectar a través del servidor de relay"), ("Failed to make direct connection to remote desktop", "No s'ha pogut establir una connexió directa amb l'escriptori remot"), - ("Set Password", "Configurar la contrasenya"), + ("Set Password", "Configura la contrasenya"), ("OS Password", "contrasenya del sistema operatiu"), ("install_tip", ""), - ("Click to upgrade", "Clicar per actualitzar"), - ("Click to download", "Clicar per descarregar"), - ("Click to update", "Clicar per refrescar"), - ("Configure", "Configurar"), + ("Click to upgrade", "Clica per a actualitzar"), + ("Click to download", "Clica per a descarregar"), + ("Click to update", "Clica per a refrescar"), + ("Configure", "Configurr"), ("config_acc", ""), ("config_screen", "Configurar pantalla"), ("Installing ...", "Instal·lant ..."), - ("Install", "Instal·lar"), + ("Install", "Instal·la"), ("Installation", "Instal·lació"), ("Installation Path", "Ruta d'instal·lació"), ("Create start menu shortcuts", "Crear accessos directes al menú d'inici"), ("Create desktop icon", "Crear icona d'escriptori"), ("agreement_tip", ""), - ("Accept and Install", "Acceptar i instal·lar"), - ("End-user license agreement", "Acord de llicència d'usuario final"), + ("Accept and Install", "Acceptar i instal·la"), + ("End-user license agreement", "Acord de llicència d'usuari final"), ("Generating ...", "Generant ..."), - ("Your installation is lower version.", "La seva instal·lació és una versión inferior."), + ("Your installation is lower version.", "La teva instal·lació és una versión inferior."), ("not_close_tcp_tip", ""), ("Listening ...", "Escoltant..."), ("Remote Host", "Hoste remot"), ("Remote Port", "Port remot"), ("Action", "Acció"), - ("Add", "Afegirr"), + ("Add", "Afegeix"), ("Local Port", "Port local"), ("Local Address", "Adreça Local"), - ("Change Local Port", "Canviar Port Local"), + ("Change Local Port", "Canvia el port local"), ("setup_server_tip", ""), ("Too short, at least 6 characters.", "Massa curt, almenys 6 caràcters."), - ("The confirmation is not identical.", "La confirmación no coincideix."), + ("The confirmation is not identical.", "La confirmació no coincideix."), ("Permissions", "Permisos"), - ("Accept", "Acceptar"), - ("Dismiss", "Cancel·lar"), - ("Disconnect", "Desconnectar"), - ("Enable file copy and paste", "Permetre copiar i enganxar arxius"), + ("Accept", "Accepta"), + ("Dismiss", "Cancel·la"), + ("Disconnect", "Desconnecta"), + ("Enable file copy and paste", "Permet copiar i enganxar fitxers"), ("Connected", "Connectat"), ("Direct and encrypted connection", "Connexió directa i xifrada"), - ("Relayed and encrypted connection", "connexió retransmesa i xifrada"), - ("Direct and unencrypted connection", "connexió directa i sense xifrar"), - ("Relayed and unencrypted connection", "connexió retransmesa i sense xifrar"), - ("Enter Remote ID", "Introduixi l'ID remot"), - ("Enter your password", "Introdueixi la seva contrasenya"), + ("Relayed and encrypted connection", "Connexió retransmesa i xifrada"), + ("Direct and unencrypted connection", "Connexió directa i sense xifrar"), + ("Relayed and unencrypted connection", "Connexió retransmesa i sense xifrar"), + ("Enter Remote ID", "Introdueix l'ID remot"), + ("Enter your password", "Introdueix la teva contrasenya"), ("Logging in...", "Iniciant sessió..."), - ("Enable RDP session sharing", "Habilitar l'ús compartit de sessions RDP"), + ("Enable RDP session sharing", "Habilita l'ús compartit de sessions RDP"), ("Auto Login", "Inici de sessió automàtic"), - ("Enable direct IP access", "Habilitar accés IP directe"), - ("Rename", "Renombrar"), + ("Enable direct IP access", "Habilita accés IP directe"), + ("Rename", "Renombra"), ("Space", "Espai"), - ("Create desktop shortcut", "Crear accés directe a l'escriptori"), - ("Change Path", "Cnviar ruta"), - ("Create Folder", "Crear carpeta"), - ("Please enter the folder name", "Indiqui el nom de la carpeta"), + ("Create desktop shortcut", "Crea accés directe a l'escriptori"), + ("Change Path", "Canvia la ruta"), + ("Create Folder", "Crea carpeta"), + ("Please enter the folder name", "Indica el nom de la carpeta"), ("Fix it", "Soluciona-ho"), ("Warning", "Avís"), ("Login screen using Wayland is not supported", "La pantalla d'inici de sessió amb Wayland no és compatible"), @@ -209,12 +209,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Invalid port", "Port incorrecte"), ("Closed manually by the peer", "Tancat manualment pel peer"), ("Enable remote configuration modification", "Habilitar modificació remota de configuració"), - ("Run without install", "Executar sense instal·lar"), + ("Run without install", "Executa sense instal·lar"), ("Connect via relay", "Connecta per relay"), ("Always connect via relay", "Connecta sempre a través de relay"), ("whitelist_tip", ""), ("Login", "Inicia sessió"), - ("Verify", "Verificar"), + ("Verify", "Verifica"), ("Remember me", "Recorda'm"), ("Trust this device", "Confia en aquest dispositiu"), ("Verification code", "Codi de verificació"), @@ -243,31 +243,31 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Discovered", "Descobert"), ("install_daemon_tip", ""), ("Remote ID", "ID remot"), - ("Paste", "Enganxar"), - ("Paste here?", "Enganxar aquí?"), - ("Are you sure to close the connection?", "Estàs segur que vols tancar la connexió?"), - ("Download new version", "Descarregar nova versió"), + ("Paste", "Enganxa"), + ("Paste here?", "Enganxa-ho aquí?"), + ("Are you sure to close the connection?", "Segur que vols tancar la connexió?"), + ("Download new version", "Descarrega una nova versió"), ("Touch mode", "Mode tàctil"), ("Mouse mode", "Mode ratolí"), - ("One-Finger Tap", "Toqui amb un dit"), + ("One-Finger Tap", "Toca amb un dit"), ("Left Mouse", "Ratolí esquerra"), ("One-Long Tap", "Toc llarg"), ("Two-Finger Tap", "Toqui amb dos dits"), ("Right Mouse", "Botó dret"), ("One-Finger Move", "Moviment amb un dir"), - ("Double Tap & Move", "Toqui dos cops i mogui"), - ("Mouse Drag", "Arrastri amb el ratolí"), + ("Double Tap & Move", "Toca dos cops i mogui"), + ("Mouse Drag", "Arrossega amb el ratolí"), ("Three-Finger vertically", "Tres dits verticalment"), ("Mouse Wheel", "Roda del ratolí"), ("Two-Finger Move", "Moviment amb dos dits"), ("Canvas Move", "Moviment del llenç"), ("Pinch to Zoom", "Pessiga per fer zoom"), - ("Canvas Zoom", "Ampliar llenç"), - ("Reset canvas", "Reestablir llenç"), + ("Canvas Zoom", "Amplia el llenç"), + ("Reset canvas", "Reestableix el llenç"), ("No permission of file transfer", "No tens permís de transferència de fitxers"), ("Note", "Nota"), - ("Connection", "connexió"), - ("Share Screen", "Compartir pantalla"), + ("Connection", "Connexió"), + ("Share Screen", "Comparteix la pantalla"), ("Chat", "Xat"), ("Total", "Total"), ("items", "ítems"), @@ -275,13 +275,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Screen Capture", "Captura de pantalla"), ("Input Control", "Control d'entrada"), ("Audio Capture", "Captura d'àudio"), - ("File Connection", "connexió d'arxius"), - ("Screen Connection", "connexió de pantalla"), + ("File Connection", "Connexió d'arxius"), + ("Screen Connection", "Connexió de pantalla"), ("Do you accept?", "Acceptes?"), ("Open System Setting", "Configuració del sistema obert"), ("How to get Android input permission?", "Com obtenir el permís d'entrada d'Android?"), ("android_input_permission_tip1", "Per a que un dispositiu remot controli el seu dispositiu Android amb el ratolí o tocs, cal permetre que RustDesk utilitzi el servei d' \"Accesibilitat\"."), - ("android_input_permission_tip2", "Vagi a la pàgina de [Serveis instal·lats], activi el servici [RustDesk Input]."), + ("android_input_permission_tip2", "Vés a la pàgina de [Serveis instal·lats], activa el servei [RustDesk Input]."), ("android_new_connection_tip", "S'ha rebut una nova sol·licitud de control per al dispositiu actual."), ("android_service_will_start_tip", "Habilitar la captura de pantalla iniciarà el servei automàticament, i permetrà que altres dispositius sol·licitin una connexió des d'aquest dispositiu."), ("android_stop_service_tip", "Tancar el servei tancarà totes les connexions establertes."), @@ -289,49 +289,49 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("android_start_service_tip", ""), ("android_permission_may_not_change_tip", ""), ("Account", "Compte"), - ("Overwrite", "Sobreescriure"), - ("This file exists, skip or overwrite this file?", "Aquest arxiu ja existeix, ometre o sobreescriure l'arxiu?"), - ("Quit", "Sortir"), + ("Overwrite", "Sobreescriu"), + ("This file exists, skip or overwrite this file?", "Aquest fitxer ja existeix, ometre o sobreescriure l'arxiu?"), + ("Quit", "Surt"), ("Help", "Ajuda"), ("Failed", "Ha fallat"), ("Succeeded", "Aconseguit"), - ("Someone turns on privacy mode, exit", "Algú ha activat el mode de privacitat, surti"), + ("Someone turns on privacy mode, exit", "Algú ha activat el mode de privacitat, surt"), ("Unsupported", "No suportat"), ("Peer denied", "Peer denegat"), - ("Please install plugins", "Instal·li complements"), + ("Please install plugins", "Instal·la els complements"), ("Peer exit", "El peer ha sortit"), ("Failed to turn off", "Error en apagar"), ("Turned off", "Apagat"), ("Language", "Idioma"), ("Keep RustDesk background service", "Mantenir RustDesk com a servei en segon pla"), - ("Ignore Battery Optimizations", "Ignorar optimizacions de la bateria"), + ("Ignore Battery Optimizations", "Ignora optimizacions de la bateria"), ("android_open_battery_optimizations_tip", ""), - ("Start on boot", "Engegar en l'arrencada"), - ("Start the screen sharing service on boot, requires special permissions", "Engegar el servei de captura de pantalla en l'arrencada, requereix permisos especials"), + ("Start on boot", "Engega en l'arrencada"), + ("Start the screen sharing service on boot, requires special permissions", "Engega el servei de captura de pantalla en l'arrencada, requereix permisos especials"), ("Connection not allowed", "Connexió no disponible"), ("Legacy mode", "Mode heretat"), ("Map mode", "Mode mapa"), ("Translate mode", "Mode traduit"), - ("Use permanent password", "Utilitzar contrasenya permament"), - ("Use both passwords", "Utilitzar ambdues contrasenyas"), - ("Set permanent password", "Establir contrasenya permament"), - ("Enable remote restart", "Activar reinici remot"), - ("Restart remote device", "Reiniciar dispositiu"), - ("Are you sure you want to restart", "Està segur que vol reiniciar?"), - ("Restarting remote device", "Reiniciant dispositiu remot"), - ("remote_restarting_tip", "Dispositiu remot reiniciant, tanqui aquest missatge i tornis a connectar amb la contrasenya."), + ("Use permanent password", "Utilitza una contrasenya permament"), + ("Use both passwords", "Utilitza ambdues contrasenyas"), + ("Set permanent password", "Estableix una contrasenya permament"), + ("Enable remote restart", "Activa el reinici remot"), + ("Restart remote device", "Reinicia el dispositiu"), + ("Are you sure you want to restart", "Segur que vol reiniciar?"), + ("Restarting remote device", "Reiniciant el dispositiu remot"), + ("remote_restarting_tip", "Reiniciant el dispositiu remot, tanca aquest missatge i torna't a connectar amb la contrasenya."), ("Copied", "Copiat"), - ("Exit Fullscreen", "Sortir de la pantalla completa"), + ("Exit Fullscreen", "Surt de la pantalla completa"), ("Fullscreen", "Pantalla completa"), ("Mobile Actions", "Accions mòbils"), - ("Select Monitor", "Seleccionar monitor"), + ("Select Monitor", "Selecciona el monitor"), ("Control Actions", "Accions de control"), ("Display Settings", "Configuració de pantalla"), ("Ratio", "Relació"), ("Image Quality", "Qualitat d'imatge"), ("Scroll Style", "Estil de desplaçament"), - ("Show Toolbar", "Mostrar la barra d'eines"), - ("Hide Toolbar", "Amancar la barra d'eines"), + ("Show Toolbar", "Mostra la barra d'eines"), + ("Hide Toolbar", "Amaga la barra d'eines"), ("Direct Connection", "Connexió directa"), ("Relay Connection", "Connexió Relay"), ("Secure Connection", "Connexió segura"), @@ -346,117 +346,117 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Dark", "Fosc"), ("Light", "Clar"), ("Follow System", "Tema del sistema"), - ("Enable hardware codec", "Habilitar còdec per hardware"), - ("Unlock Security Settings", "Desbloquejar ajustaments de seguretat"), - ("Enable audio", "Habilitar àudio"), - ("Unlock Network Settings", "Desbloquejar Ajustaments de Xarxa"), + ("Enable hardware codec", "Habilita el còdec per hardware"), + ("Unlock Security Settings", "Desbloqueja els ajustaments de seguretat"), + ("Enable audio", "Habilita l'àudio"), + ("Unlock Network Settings", "Desbloqueja els ajustaments de xarxa"), ("Server", "Servidor"), ("Direct IP Access", "Accés IP Directe"), ("Proxy", "Proxy"), - ("Apply", "Aplicar"), - ("Disconnect all devices?", "Desconnectar tots els dispositius?"), - ("Clear", "Netejar"), + ("Apply", "Aplica"), + ("Disconnect all devices?", "Vols desconnectar tots els dispositius?"), + ("Clear", "Neteja"), ("Audio Input Device", "Dispositiu d'entrada d'àudio"), ("Use IP Whitelisting", "Utilitza llista de IPs admeses"), ("Network", "Xarxa"), ("Pin Toolbar", "Fixa la barra d'eines"), - ("Unpin Toolbar", "Soltar la barra d'eines"), + ("Unpin Toolbar", "Deixa de fixar la barra d'eines"), ("Recording", "Gravant"), ("Directory", "Directori"), ("Automatically record incoming sessions", "Gravació automàtica de sessions entrants"), - ("Change", "Canviar"), - ("Start session recording", "Començar gravació de sessió"), - ("Stop session recording", "Aturar gravació de sessió"), - ("Enable recording session", "Habilitar gravació de sessió"), - ("Enable LAN discovery", "Habilitar descobriment de LAN"), - ("Deny LAN discovery", "Denegar descobriment de LAN"), - ("Write a message", "Escriure un missatge"), - ("Prompt", "Consultar"), - ("Please wait for confirmation of UAC...", "Sisplau, espereu per confirmar l'UAC..."), + ("Change", "Canvia"), + ("Start session recording", "Comença la gravació de la sessió"), + ("Stop session recording", "Atura la gravació de la sessió"), + ("Enable recording session", "Habilita la gravació de la sessió"), + ("Enable LAN discovery", "Habilita el descobriment de LAN"), + ("Deny LAN discovery", "Denega el descobriment de LAN"), + ("Write a message", "Escriu un missatge"), + ("Prompt", "Consulta"), + ("Please wait for confirmation of UAC...", "Espera per confirmar l'UAC..."), ("elevated_foreground_window_tip", ""), ("Disconnected", "Desconnectat"), ("Other", "Altre"), - ("Confirm before closing multiple tabs", "Confirmar abans de tancar múltiples pestanyes"), + ("Confirm before closing multiple tabs", "Confirma abans de tancar múltiples pestanyes"), ("Keyboard Settings", "Ajustaments de teclat"), ("Full Access", "Acces complet"), - ("Screen Share", "Compartir pantalla"), + ("Screen Share", "Comparteix la pantalla"), ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland requereix Ubuntu 21.04 o una versió superior."), - ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland requereix una versió superior de la distribución de Linux. Provi l'escriptori X11 o canvïi el seu sistema operatiu."), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland requereix una versió superior de la distribución de Linux. Prova l'escriptori X11 o canvia el teu sistema operatiu."), ("JumpLink", "Veure"), - ("Please Select the screen to be shared(Operate on the peer side).", "Seleccioni la pantalla que es compartirà (Operar al costat del peer)."), - ("Show RustDesk", "Mostrar RustDesk"), + ("Please Select the screen to be shared(Operate on the peer side).", "Selecciona la pantalla que es compartirà (Opera al costat del peer)."), + ("Show RustDesk", "Mostra RustDesk"), ("This PC", "Aquest PC"), ("or", "o"), - ("Continue with", "Continuar amb"), - ("Elevate", "Elevar"), + ("Continue with", "Continur amb"), + ("Elevate", "Eleva"), ("Zoom cursor", "Zoom del ratolí"), - ("Accept sessions via password", "Acceptar sessions via contrasenya"), - ("Accept sessions via click", "Acceptar sessions via clic"), - ("Accept sessions via both", "Acceptar sessions via les dues opcions"), - ("Please wait for the remote side to accept your session request...", "Sisplau, espereu que la part remota accepti la teva sol·licitud de sessió..."), + ("Accept sessions via password", "Accepta sessions via contrasenya"), + ("Accept sessions via click", "Accepta sessions via clic"), + ("Accept sessions via both", "Accepta sessions via les dues opcions"), + ("Please wait for the remote side to accept your session request...", "Esperea que la part remota accepti la teva sol·licitud de sessió..."), ("One-time Password", "Contrasenya d'un sol ús"), - ("Use one-time password", "Fer ser una contrasenya d'un sol ús"), + ("Use one-time password", "Fes servir una contrasenya d'un sol ús"), ("One-time password length", "Caracters de la contrasenya d'un sol ús"), - ("Request access to your device", "Sol·licitar l'acces al vostre dispositiu"), - ("Hide connection management window", "Amagar la finestra de gestió de connexió"), + ("Request access to your device", "Sol·licita l'acces al teu dispositiu"), + ("Hide connection management window", "Amaga la finestra de gestió de connexió"), ("hide_cm_tip", ""), ("wayland_experiment_tip", ""), - ("Right click to select tabs", "Clic dret per seleccionar pestanyes"), - ("Skipped", "Pasat"), - ("Add to address book", "Afegir a la llibreta d'adreces"), + ("Right click to select tabs", "Clic dret per seleccionar les pestanyes"), + ("Skipped", "Saltat"), + ("Add to address book", "Afegeix a la llibreta d'adreces"), ("Group", "Grup"), - ("Search", "Cercar"), + ("Search", "Cerca"), ("Closed manually by web console", "Tancat manualment amb la consola web"), ("Local keyboard type", "Tipus de teclat local"), - ("Select local keyboard type", "Seleccionar el tipus de teclat local"), + ("Select local keyboard type", "Selecciona el tipus de teclat local"), ("software_render_tip", ""), - ("Always use software rendering", "Sempre fer servir renderització per software"), + ("Always use software rendering", "Fes servir sempre la renderització per software"), ("config_input", ""), ("config_microphone", ""), ("request_elevation_tip", ""), - ("Wait", "Espereu"), - ("Elevation Error", "Error de elevació"), + ("Wait", "Espera"), + ("Elevation Error", "Error d'elevació"), ("Ask the remote user for authentication", "Demana autenticació a l'usuari remot"), - ("Choose this if the remote account is administrator", "Seleccionar això si l'usuari remot es administrador"), + ("Choose this if the remote account is administrator", "Selecciona això si l'usuari remot és administrador"), ("Transmit the username and password of administrator", "Transmet el nom d'usuari i la contrasenya de l'administrador"), ("still_click_uac_tip", ""), - ("Request Elevation", "Demanar l'elevació"), + ("Request Elevation", "Demana l'elevació"), ("wait_accept_uac_tip", ""), ("Elevate successfully", "Elevació exitosa"), ("uppercase", "majúscula"), ("lowercase", "minúscula"), ("digit", "dígit"), ("special character", "caràcter especial"), - ("length>=8", ""), + ("length>=8", "longitut>=8"), ("Weak", "Dèbil"), - ("Medium", "Media"), + ("Medium", "Mitja"), ("Strong", "Forta"), - ("Switch Sides", "Canviar l'esquerra i la dreta"), - ("Please confirm if you want to share your desktop?", "Sisplau, confirmeu si voleu compartir el seu escriptori?"), + ("Switch Sides", "Canvia de costat"), + ("Please confirm if you want to share your desktop?", "Confirma que vols compartir el teu escriptori?"), ("Display", "Pantalla"), ("Default View Style", "Estil de visualització predeterminat"), ("Default Scroll Style", "Estil de desplaçament predeterminat"), ("Default Image Quality", "Qualitat d'imatge predeterminada"), ("Default Codec", "Còdec predeterminat"), ("Bitrate", "Ratio de bits"), - ("FPS", ""), + ("FPS", "FPS"), ("Auto", "Auto"), ("Other Default Options", "Altres opcions predeterminades"), ("Voice call", "Trucada de veu"), ("Text chat", "Xat de text"), - ("Stop voice call", "Penjar la trucada de veu"), + ("Stop voice call", "Penja la trucada de veu"), ("relay_hint_tip", ""), - ("Reconnect", "Reconectar"), + ("Reconnect", "Reconecta"), ("Codec", "Còdec"), ("Resolution", "Resolució"), ("No transfers in progress", "Sense transferències en curs"), - ("Set one-time password length", "Seleccionar la longitud de la contrasenya d'un sol ús"), + ("Set one-time password length", "Selecciona la longitud de la contrasenya d'un sol ús"), ("RDP Settings", "Configuració RDP"), - ("Sort by", "Ordenar per"), + ("Sort by", "Ordena per"), ("New Connection", "Nova connexió"), - ("Restore", "Restaurar"), - ("Minimize", "Minimizar"), - ("Maximize", "Maximizar"), + ("Restore", "Restaura"), + ("Minimize", "Minimitza"), + ("Maximize", "Maximtiza"), ("Your Device", "El teu dispositiu"), ("empty_recent_tip", ""), ("empty_favorite_tip", ""), @@ -480,98 +480,98 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("xorg_not_found_text_tip", ""), ("no_desktop_title_tip", ""), ("no_desktop_text_tip", ""), - ("No need to elevate", ""), + ("No need to elevate", "No cal elevar permisos"), ("System Sound", "Sistema de so"), ("Default", "Predeterminat"), ("New RDP", "Nou RDP"), ("Fingerprint", "Empremta digital"), - ("Copy Fingerprint", "Copiar l'emprenta digital"), + ("Copy Fingerprint", "Copia l'emprenta digital"), ("no fingerprints", "sense emprentes"), - ("Select a peer", "Seleccionar un peer"), - ("Select peers", "Seleccionar varios peers"), - ("Plugins", "Plugins"), - ("Uninstall", "Desinstal·lar"), - ("Update", "Actualitzar"), - ("Enable", "Activar"), - ("Disable", "Desactivar"), + ("Select a peer", "Selecciona un peer"), + ("Select peers", "Selecciona diversos peers"), + ("Plugins", "Complements"), + ("Uninstall", "Desinstal·la"), + ("Update", "Actualitza"), + ("Enable", "Activa"), + ("Disable", "Desactiva"), ("Options", "Opcions"), ("resolution_original_tip", ""), ("resolution_fit_local_tip", ""), ("resolution_custom_tip", ""), - ("Collapse toolbar", "Col·lapsar la barra d'etiquetes"), - ("Accept and Elevate", "Aceptar i elevar"), + ("Collapse toolbar", "Col·lapsa la barra d'etiquetes"), + ("Accept and Elevate", "Accepta i eleva"), ("accept_and_elevate_btn_tooltip", ""), ("clipboard_wait_response_timeout_tip", ""), ("Incoming connection", "Connexió entrant"), ("Outgoing connection", "Connexió sortint"), - ("Exit", "Tancar"), - ("Open", "Obrir"), + ("Exit", "Tanca"), + ("Open", "Obre"), ("logout_tip", ""), - ("Service", "Servici"), - ("Start", "Iniciar"), - ("Stop", "Aturar"), + ("Service", "Servei"), + ("Start", "Inicia"), + ("Stop", "Atura"), ("exceed_max_devices", ""), - ("Sync with recent sessions", "Sincronitzar amb les sessions recents"), - ("Sort tags", "Ordenar per etiquetes"), - ("Open connection in new tab", "Obrir connexió en una nova pestanya"), + ("Sync with recent sessions", "Sincronitza amb les sessions recents"), + ("Sort tags", "Ordena per etiquetes"), + ("Open connection in new tab", "Obre la connexió en una nova pestanya"), ("Move tab to new window", "Mou la pestanya a una nova finestra"), ("Can not be empty", "No pot estar buit"), ("Already exists", "Ja existeix"), - ("Change Password", "Canviar la contrasenya"), - ("Refresh Password", "Refrescar la contrasenya"), + ("Change Password", "Canvia la contrasenya"), + ("Refresh Password", "Refresca la contrasenya"), ("ID", "ID"), - ("Grid View", "Visualització de grilla"), + ("Grid View", "Visualització de graella"), ("List View", "Visualització de llista"), - ("Select", "Seleccionar"), - ("Toggle Tags", "Activar/desactivar etiquetes"), + ("Select", "Selecciona"), + ("Toggle Tags", "Activa/desactiva les etiquetes"), ("pull_ab_failed_tip", ""), ("push_ab_failed_tip", ""), ("synced_peer_readded_tip", ""), - ("Change Color", "Canviar el color"), + ("Change Color", "Canvia el color"), ("Primary Color", "Color primari"), ("HSV Color", "Color HSV"), ("Installation Successful!", "Instal·lació correcta!"), - ("Installation failed!", "Instal·lació fallada!"), - ("Reverse mouse wheel", "Canviar la orientació de la roda del ratolí"), + ("Installation failed!", "Ha fallat la instal·lació!"), + ("Reverse mouse wheel", "Canvia l'orientació de la roda del ratolí"), ("{} sessions", "{} sessions"), ("scam_title", ""), ("scam_text1", ""), ("scam_text2", ""), - ("Don't show again", "No mostrar més"), - ("I Agree", "D'acord"), - ("Decline", "Rebutjar"), + ("Don't show again", "No ho mostris més"), + ("I Agree", "Accepta"), + ("Decline", "Rebutja"), ("Timeout in minutes", "Desconexió en minuts"), ("auto_disconnect_option_tip", ""), ("Connection failed due to inactivity", "Connexió fallada per inactivitat"), - ("Check for software update on startup", "Revisar actualitzacions de software al iniciar"), + ("Check for software update on startup", "Revisa les actualitzacions de software en iniciar"), ("upgrade_rustdesk_server_pro_to_{}_tip", ""), ("pull_group_failed_tip", ""), - ("Filter by intersection", "Filtrar per intersecció"), - ("Remove wallpaper during incoming sessions", "Amagar el fons de pantalla en les connexions entrants"), + ("Filter by intersection", "Filtra per intersecció"), + ("Remove wallpaper during incoming sessions", "Amaga el fons de pantalla en les connexions entrants"), ("Test", "Prova"), ("display_is_plugged_out_msg", ""), ("No displays", "Sense pantalles"), - ("Open in new window", "Obrir en una nova finestra"), - ("Show displays as individual windows", "Mostrar pantalles com finestres individuals"), - ("Use all my displays for the remote session", "Fer servir totes las meves pantalles per la sessió remota"), + ("Open in new window", "Obre en una nova finestra"), + ("Show displays as individual windows", "Mostra les pantalles com finestres individuals"), + ("Use all my displays for the remote session", "Fes servir totes les meves pantalles per la sessió remota"), ("selinux_tip", ""), - ("Change view", "Canviar la vista"), + ("Change view", "Canvia la vista"), ("Big tiles", "Títols grans"), ("Small tiles", "Títols petits"), ("List", "Llista"), ("Virtual display", "Pantalla virtual"), ("Plug out all", "Desconnectar tots"), ("True color (4:4:4)", "Color real (4:4:4)"), - ("Enable blocking user input", "Activar bloqueig d'entrada d'usuari"), + ("Enable blocking user input", "Activa el bloqueig d'entrada d'usuari"), ("id_input_tip", ""), ("privacy_mode_impl_mag_tip", ""), ("privacy_mode_impl_virtual_display_tip", ""), - ("Enter privacy mode", "Activar el mode de privacitat"), - ("Exit privacy mode", "Sortir del mode de privacitat"), + ("Enter privacy mode", "Entra al mode de privacitat"), + ("Exit privacy mode", "Surt del mode de privacitat"), ("idd_not_support_under_win10_2004_tip", ""), ("input_source_1_tip", ""), ("input_source_2_tip", ""), - ("Swap control-command key", "Canviar la tecla de control"), + ("Swap control-command key", "Canvia la tecla de control"), ("swap-left-right-mouse", ""), ("2FA code", "Codi 2FA"), ("More", "Més"), @@ -582,7 +582,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Email verification code must be 6 characters.", "El codi de verificació de l'email ha de tenir 6 caràcters."), ("2FA code must be 6 digits.", "El codi 2FA ha de tenir 6 digits."), ("Multiple Windows sessions found", "Multiples sessions de Windows trobades"), - ("Please select the session you want to connect to", "Sisplau, seleccioni la sessió que voleu connectar"), + ("Please select the session you want to connect to", "Selecciona la sessió a la què et vols connectar"), ("powered_by_me", ""), ("outgoing_only_desk_tip", ""), ("preset_password_warning", ""), @@ -606,24 +606,24 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("no_audio_input_device_tip", ""), ("Incoming", "Entrant"), ("Outgoing", "Sortint"), - ("Clear Wayland screen selection", "Netejar la selecció de pantalla Wayland"), + ("Clear Wayland screen selection", "Neteja la selecció de pantalla Wayland"), ("clear_Wayland_screen_selection_tip", ""), ("confirm_clear_Wayland_screen_selection_tip", ""), ("android_new_voice_call_tip", ""), ("texture_render_tip", ""), - ("Use texture rendering", "Fer servir renderització de textures"), + ("Use texture rendering", "Fes servir la renderització de textures"), ("Floating window", "Finestra flotant"), ("floating_window_tip", ""), - ("Keep screen on", "Mantenir la pantalla encesa"), + ("Keep screen on", "Deixa la pantalla encesa"), ("Never", "Mai"), ("During controlled", "Mentre estigui controlat"), - ("During service is on", "Mentres el servei estigui encés"), + ("During service is on", "Mentre el servei estigui encés"), ("Capture screen using DirectX", "Captura de pantalla utilitzant DirectX"), ("Back", "Enrere"), ("Apps", "Aplicacions"), - ("Volume up", "Pujar el volum"), + ("Volume up", "Puja el volum"), ("Volume down", "Baixa el volum"), - ("Power", "Engegar"), + ("Power", "Engega"), ("Telegram bot", "Bot de Telegram"), ("enable-bot-tip", ""), ("enable-bot-desc", ""), From 6820e2f4c78a9f04d65b730a159272a7377985eb Mon Sep 17 00:00:00 2001 From: Mr-Update <37781396+Mr-Update@users.noreply.github.com> Date: Fri, 9 Aug 2024 02:43:18 +0200 Subject: [PATCH 078/541] Update de.rs (#9004) --- src/lang/de.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/lang/de.rs b/src/lang/de.rs index fc6269cb7d6f..c31cc9d99e9c 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -22,7 +22,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("TCP tunneling", "TCP-Tunnelung"), ("Remove", "Entfernen"), ("Refresh random password", "Zufälliges Passwort erzeugen"), - ("Set your own password", "Eigenes Passwort setzen"), + ("Set your own password", "Eigenes Passwort festlegen"), ("Enable keyboard/mouse", "Tastatur und Maus aktivieren"), ("Enable clipboard", "Zwischenablage aktivieren"), ("Enable file transfer", "Dateiübertragung aktivieren"), @@ -314,7 +314,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Translate mode", "Übersetzungsmodus"), ("Use permanent password", "Permanentes Passwort verwenden"), ("Use both passwords", "Beide Passwörter verwenden"), - ("Set permanent password", "Permanentes Passwort setzen"), + ("Set permanent password", "Permanentes Passwort festlegen"), ("Enable remote restart", "Entfernten Neustart aktivieren"), ("Restart remote device", "Entferntes Gerät neu starten"), ("Are you sure you want to restart", "Möchten Sie das entfernte Gerät wirklich neu starten?"), @@ -590,7 +590,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("My address book", "Mein Adressbuch"), ("Personal", "Persönlich"), ("Owner", "Eigentümer"), - ("Set shared password", "Geteiltes Passwort setzen"), + ("Set shared password", "Geteiltes Passwort festlegen"), ("Exist in", "Existiert in …?"), ("Read-only", "Nur lesen"), ("Read/Write", "Lesen/Schreiben"), @@ -632,9 +632,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About RustDesk", "Über RustDesk"), ("Send clipboard keystrokes", "Tastenanschläge aus der Zwischenablage senden"), ("network_error_tip", "Bitte überprüfen Sie Ihre Netzwerkverbindung und versuchen Sie es dann erneut."), - ("Unlock with PIN", ""), - ("Requires at least {} characters", ""), - ("Wrong PIN", ""), - ("Set PIN", ""), + ("Unlock with PIN", "Mit PIN entsperren"), + ("Requires at least {} characters", "Erfordert mindestens {} Zeichen"), + ("Wrong PIN", "Falsche PIN"), + ("Set PIN", "PIN festlegen"), ].iter().cloned().collect(); } From 3c6ddd7403968a05a71d30e1d60f76d217e9156d Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Fri, 9 Aug 2024 21:17:06 +0800 Subject: [PATCH 079/541] fix: multi-displays, displays changed, switch idx (#9006) Use init display index as the primary index. But when displays changed, the primary display may also changes. No need to change the old primary index. But we need to make sure that the old primary index does not exceed the display number. Signed-off-by: fufesou --- flutter/lib/models/model.dart | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 15510377673b..6a900902617d 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -1003,14 +1003,15 @@ class FfiModel with ChangeNotifier { // Notify to switch display msgBox(sessionId, 'custom-nook-nocancel-hasclose-info', 'Prompt', 'display_is_plugged_out_msg', '', parent.target!.dialogManager); - final newDisplay = pi.primaryDisplay == kInvalidDisplayIndex - ? 0 - : pi.primaryDisplay; - final displays = newDisplay; + final isPeerPrimaryDisplayValid = + pi.primaryDisplay == kInvalidDisplayIndex || + pi.primaryDisplay >= pi.displays.length; + final newDisplay = + isPeerPrimaryDisplayValid ? 0 : pi.primaryDisplay; bind.sessionSwitchDisplay( isDesktop: isDesktop, sessionId: sessionId, - value: Int32List.fromList([displays]), + value: Int32List.fromList([newDisplay]), ); if (_pi.isSupportMultiUiSession) { From 7521bbe15f2a27830155c0677383241e05dd4077 Mon Sep 17 00:00:00 2001 From: bovirus <1262554+bovirus@users.noreply.github.com> Date: Sat, 10 Aug 2024 14:52:27 +0200 Subject: [PATCH 080/541] Update Italian language (#9016) --- src/lang/it.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/lang/it.rs b/src/lang/it.rs index c992d033a246..d50b963b5119 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -46,7 +46,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About", "Info programma"), ("Slogan_tip", "Realizzato con il cuore in questo mondo caotico!"), ("Privacy Statement", "Informativa sulla privacy"), - ("Mute", "Audio off"), + ("Mute", "Audio disabilitato"), ("Build Date", "Data build"), ("Version", "Versione"), ("Home", "Home"), @@ -208,7 +208,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Username", "Nome utente"), ("Invalid port", "Numero porta non valido"), ("Closed manually by the peer", "Chiuso manualmente dal dispositivo remoto"), - ("Enable remote configuration modification", "Abilita la modifica remota della configurazione"), + ("Enable remote configuration modification", "Abilita modifica remota configurazione"), ("Run without install", "Esegui senza installare"), ("Connect via relay", "Collegati tramite relay"), ("Always connect via relay", "Collegati sempre tramite relay"), @@ -541,19 +541,19 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("I Agree", "Accetto"), ("Decline", "Non accetto"), ("Timeout in minutes", "Timeout in minuti"), - ("auto_disconnect_option_tip", "Chiudi automaticamente sessioni in entrata in caso di inattività utente"), + ("auto_disconnect_option_tip", "Chiudi automaticamente sessioni in entrata per inattività utente"), ("Connection failed due to inactivity", "Connessione non riuscita a causa di inattività"), ("Check for software update on startup", "All'avvio verifica presenza aggiornamenti programma"), ("upgrade_rustdesk_server_pro_to_{}_tip", "Aggiorna RustDesk Server Pro alla versione {} o successiva!"), ("pull_group_failed_tip", "Impossibile aggiornare il gruppo"), ("Filter by intersection", "Filtra per incrocio"), - ("Remove wallpaper during incoming sessions", "Rimuovi lo sfondo durante le sessioni in entrata"), + ("Remove wallpaper during incoming sessions", "Rimuovi sfondo durante sessioni in entrata"), ("Test", "Test"), ("display_is_plugged_out_msg", "Lo schermo è scollegato, passo al primo schermo."), ("No displays", "Nessuno schermo"), ("Open in new window", "Apri in una nuova finestra"), ("Show displays as individual windows", "Visualizza schermi come finestre individuali"), - ("Use all my displays for the remote session", "Usa tutti gli schermi per la sessione remota"), + ("Use all my displays for the remote session", "Nella sessione remota usa tutti gli schermi"), ("selinux_tip", "In questo dispositivo è abilitato SELinux, che potrebbe impedire il corretto funzionamento di RustDesk come lato controllato."), ("Change view", "Modifica vista"), ("Big tiles", "Icone grandi"), @@ -598,7 +598,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("share_warning_tip", "I campi sopra indicati sono condivisi e visibili ad altri."), ("Everyone", "Everyone"), ("ab_web_console_tip", "Altre info sulla console web"), - ("allow-only-conn-window-open-tip", "Consenti la connessione solo se la finestra RustDesk è aperta"), + ("allow-only-conn-window-open-tip", "Consenti connessione solo se la finestra RustDesk è aperta"), ("no_need_privacy_mode_no_physical_displays_tip", "Nessun display fisico, nessuna necessità di usare la modalità privacy."), ("Follow remote cursor", "Segui cursore remoto"), ("Follow remote window focus", "Segui focus finestra remota"), From fd69b146236eaa99dfecad0f32bdf95ce0e43b45 Mon Sep 17 00:00:00 2001 From: mehdi-song Date: Sun, 11 Aug 2024 02:19:34 +0330 Subject: [PATCH 081/541] Update fa.rs (#9021) --- src/lang/fa.rs | 44 ++++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/src/lang/fa.rs b/src/lang/fa.rs index e9c77bd69237..8166e0f924ca 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -610,31 +610,31 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("clear_Wayland_screen_selection_tip", "پس از پاک کردن صفحه انتخابی، می توانید صفحه را برای اشتراک گذاری مجدد انتخاب کنید"), ("confirm_clear_Wayland_screen_selection_tip", "را پاک می کنید؟ Wayland آیا مطمئن هستید که انتخاب صفحه"), ("android_new_voice_call_tip", "یک درخواست تماس صوتی جدید دریافت شد. اگر بپذیرید، صدا به ارتباط صوتی تغییر خواهد کرد."), - ("texture_render_tip", ""), - ("Use texture rendering", ""), - ("Floating window", ""), - ("floating_window_tip", ""), - ("Keep screen on", ""), - ("Never", ""), + ("texture_render_tip", "از رندر بافت برای صاف کردن تصاویر استفاده کنید. اگر با مشکل رندر مواجه شدید، می توانید این گزینه را غیرفعال کنید."), + ("Use texture rendering", "از رندر بافت استفاده کنید"), + ("Floating window", "پنجره شناور"), + ("floating_window_tip", "کمک می کند RustDesk این به حفظ سرویس پس زمینه"), + ("Keep screen on", "صفحه نمایش را روشن نگه دارید"), + ("Never", "هرگز"), ("During controlled", ""), ("During service is on", ""), - ("Capture screen using DirectX", ""), - ("Back", ""), - ("Apps", ""), - ("Volume up", ""), - ("Volume down", ""), + ("Capture screen using DirectX", "DirectX تصویربرداری از صفحه نمایش با استفاده از"), + ("Back", "برگشت"), + ("Apps", "برنامه ها"), + ("Volume up", "افزایش صدا"), + ("Volume down", "کاهش صدا"), ("Power", ""), - ("Telegram bot", ""), - ("enable-bot-tip", ""), + ("Telegram bot", "ربات تلگرام"), + ("enable-bot-tip", "اگر این ویژگی را فعال کنید، می توانید کد تائید دو مرحله ای را از ربات خود دریافت کنید. همچنین می تواند به عنوان یک اعلان اتصال عمل کند."), ("enable-bot-desc", ""), - ("cancel-2fa-confirm-tip", ""), - ("cancel-bot-confirm-tip", ""), - ("About RustDesk", ""), - ("Send clipboard keystrokes", ""), - ("network_error_tip", ""), - ("Unlock with PIN", ""), - ("Requires at least {} characters", ""), - ("Wrong PIN", ""), - ("Set PIN", ""), + ("cancel-2fa-confirm-tip", "آیا مطمئن هستید که می خواهید تائید دو مرحله ای را لغو کنید؟"), + ("cancel-bot-confirm-tip", "آیا مطمئن هستید که می خواهید ربات تلگرام را لغو کنید؟"), + ("About RustDesk", "RustDesk درباره"), + ("Send clipboard keystrokes", "ارسال کلیدهای کلیپ بورد"), + ("network_error_tip", "لطفاً اتصال شبکه خود را بررسی کنید، سپس روی امتحان مجدد کلیک کنید."), + ("Unlock with PIN", "باز کردن قفل با پین"), + ("Requires at least {} characters", "حداقل به {} کاراکترها نیاز دارد"), + ("Wrong PIN", "پین اشتباه است"), + ("Set PIN", "پین را تنظیم کنید"), ].iter().cloned().collect(); } From ce56be65077602d6fa4713ecb8d80d7ce6e71d42 Mon Sep 17 00:00:00 2001 From: FastAct <93490087+FastAct@users.noreply.github.com> Date: Sun, 11 Aug 2024 00:49:52 +0200 Subject: [PATCH 082/541] Update nl.rs (#9022) --- src/lang/nl.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/lang/nl.rs b/src/lang/nl.rs index 764849991c83..187611393195 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -632,9 +632,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About RustDesk", "Over RustDesk"), ("Send clipboard keystrokes", "Klembord toetsaanslagen verzenden"), ("network_error_tip", "Controleer de netwerkverbinding en selecteer 'Opnieuw proberen'."), - ("Unlock with PIN", ""), - ("Requires at least {} characters", ""), - ("Wrong PIN", ""), - ("Set PIN", ""), + ("Unlock with PIN", "Ontgrendelen met PIN"), + ("Requires at least {} characters", "Vereist minstens {} tekens"), + ("Wrong PIN", "Verkeerde PIN-code"), + ("Set PIN", "PIN-code instellen"), ].iter().cloned().collect(); } From 6625aca9942601fcc97049419f2bfd82d9575f91 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Sun, 11 Aug 2024 19:26:41 +0800 Subject: [PATCH 083/541] fix: win, virtual display (#9023) 1. Default resolution 1920x1080. 2. Restore on conn & disconn. Signed-off-by: fufesou --- src/client/io_loop.rs | 19 ++++ src/flutter.rs | 48 ++++++++++ src/flutter_ffi.rs | 1 + src/platform/windows.rs | 104 ++++++++++++++++++++ src/privacy_mode/win_virtual_display.rs | 121 +++--------------------- src/server/connection.rs | 2 +- src/server/display_service.rs | 1 - src/virtual_display_manager.rs | 77 ++++++++------- 8 files changed, 229 insertions(+), 144 deletions(-) diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index ea2afff1dd7e..ce32b0d26899 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -937,6 +937,24 @@ impl Remote { } } + async fn send_toggle_virtual_display_msg(&self, peer: &mut Stream) { + let lc = self.handler.lc.read().unwrap(); + let displays = lc.get_option("virtual-display"); + for d in displays.split(',') { + if let Ok(index) = d.parse::() { + let mut misc = Misc::new(); + misc.set_toggle_virtual_display(ToggleVirtualDisplay { + display: index, + on: true, + ..Default::default() + }); + let mut msg_out = Message::new(); + msg_out.set_misc(misc); + allow_err!(peer.send(&msg_out).await); + } + } + } + async fn send_toggle_privacy_mode_msg(&self, peer: &mut Stream) { let lc = self.handler.lc.read().unwrap(); if lc.version >= hbb_common::get_version_number("1.2.4") @@ -1073,6 +1091,7 @@ impl Remote { self.handler.close_success(); self.handler.adapt_size(); self.send_opts_after_login(peer).await; + self.send_toggle_virtual_display_msg(peer).await; self.send_toggle_privacy_mode_msg(peer).await; } let incoming_format = CodecFormat::from(&vf); diff --git a/src/flutter.rs b/src/flutter.rs index e60063357a00..d408202a96d6 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -1780,6 +1780,54 @@ pub fn try_sync_peer_option( } } +pub(super) fn session_update_virtual_display(session: &FlutterSession, index: i32, on: bool) { + let virtual_display_key = "virtual-display"; + let displays = session.get_option(virtual_display_key.to_owned()); + if !on { + if index == -1 { + if !displays.is_empty() { + session.set_option(virtual_display_key.to_owned(), "".to_owned()); + } + } else { + let mut vdisplays = displays.split(',').collect::>(); + let len = vdisplays.len(); + if index == 0 { + // 0 means we cann't toggle the virtual display by index. + vdisplays.remove(vdisplays.len() - 1); + } else { + if let Some(i) = vdisplays.iter().position(|&x| x == index.to_string()) { + vdisplays.remove(i); + } + } + if vdisplays.len() != len { + session.set_option( + virtual_display_key.to_owned(), + vdisplays.join(",").to_owned(), + ); + } + } + } else { + let mut vdisplays = displays + .split(',') + .map(|x| x.to_string()) + .collect::>(); + let len = vdisplays.len(); + if index == 0 { + vdisplays.push(index.to_string()); + } else { + if !vdisplays.iter().any(|x| *x == index.to_string()) { + vdisplays.push(index.to_string()); + } + } + if vdisplays.len() != len { + session.set_option( + virtual_display_key.to_owned(), + vdisplays.join(",").to_owned(), + ); + } + } +} + // sessions mod is used to avoid the big lock of sessions' map. pub mod sessions { use std::collections::HashSet; diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 7fb324b3f4f0..d64823b8dc6a 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -1568,6 +1568,7 @@ pub fn session_on_waiting_for_image_dialog_show(session_id: SessionID) { pub fn session_toggle_virtual_display(session_id: SessionID, index: i32, on: bool) { if let Some(session) = sessions::get_session_by_session_id(&session_id) { session.toggle_virtual_display(index, on); + flutter::session_update_virtual_display(&session, index, on); } } diff --git a/src/platform/windows.rs b/src/platform/windows.rs index 51aee97a3799..e11f7d110ec9 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -2595,3 +2595,107 @@ pub fn try_set_window_foreground(window: HWND) { } } } + +pub mod reg_display_settings { + use hbb_common::ResultType; + use serde_derive::{Deserialize, Serialize}; + use std::collections::HashMap; + use winreg::{enums::*, RegValue}; + const REG_GRAPHICS_DRIVERS_PATH: &str = "SYSTEM\\CurrentControlSet\\Control\\GraphicsDrivers"; + const REG_CONNECTIVITY_PATH: &str = "Connectivity"; + + #[derive(Serialize, Deserialize, Debug)] + pub struct RegRecovery { + path: String, + key: String, + old: (Vec, isize), + new: (Vec, isize), + } + + pub fn read_reg_connectivity() -> ResultType>> + { + let hklm = winreg::RegKey::predef(HKEY_LOCAL_MACHINE); + let reg_connectivity = hklm.open_subkey_with_flags( + format!("{}\\{}", REG_GRAPHICS_DRIVERS_PATH, REG_CONNECTIVITY_PATH), + KEY_READ, + )?; + + let mut map_connectivity = HashMap::new(); + for key in reg_connectivity.enum_keys() { + let key = key?; + let mut map_item = HashMap::new(); + let reg_item = reg_connectivity.open_subkey_with_flags(&key, KEY_READ)?; + for value in reg_item.enum_values() { + let (name, value) = value?; + map_item.insert(name, value); + } + map_connectivity.insert(key, map_item); + } + Ok(map_connectivity) + } + + pub fn diff_recent_connectivity( + map1: HashMap>, + map2: HashMap>, + ) -> Option { + for (subkey, map_item2) in map2 { + if let Some(map_item1) = map1.get(&subkey) { + let key = "Recent"; + if let Some(value1) = map_item1.get(key) { + if let Some(value2) = map_item2.get(key) { + if value1 != value2 { + return Some(RegRecovery { + path: format!( + "{}\\{}\\{}", + REG_GRAPHICS_DRIVERS_PATH, REG_CONNECTIVITY_PATH, subkey + ), + key: key.to_owned(), + old: (value1.bytes.clone(), value1.vtype.clone() as isize), + new: (value2.bytes.clone(), value2.vtype.clone() as isize), + }); + } + } + } + } + } + None + } + + pub fn restore_reg_connectivity(reg_recovery: RegRecovery) -> ResultType<()> { + let hklm = winreg::RegKey::predef(HKEY_LOCAL_MACHINE); + let reg_item = hklm.open_subkey_with_flags(®_recovery.path, KEY_READ | KEY_WRITE)?; + let cur_reg_value = reg_item.get_raw_value(®_recovery.key)?; + let new_reg_value = RegValue { + bytes: reg_recovery.new.0, + vtype: isize_to_reg_type(reg_recovery.new.1), + }; + if cur_reg_value != new_reg_value { + return Ok(()); + } + let reg_value = RegValue { + bytes: reg_recovery.old.0, + vtype: isize_to_reg_type(reg_recovery.old.1), + }; + reg_item.set_raw_value(®_recovery.key, ®_value)?; + Ok(()) + } + + #[inline] + fn isize_to_reg_type(i: isize) -> RegType { + match i { + 0 => RegType::REG_NONE, + 1 => RegType::REG_SZ, + 2 => RegType::REG_EXPAND_SZ, + 3 => RegType::REG_BINARY, + 4 => RegType::REG_DWORD, + 5 => RegType::REG_DWORD_BIG_ENDIAN, + 6 => RegType::REG_LINK, + 7 => RegType::REG_MULTI_SZ, + 8 => RegType::REG_RESOURCE_LIST, + 9 => RegType::REG_FULL_RESOURCE_DESCRIPTOR, + 10 => RegType::REG_RESOURCE_REQUIREMENTS_LIST, + 11 => RegType::REG_QWORD, + _ => RegType::REG_NONE, + } + } +} diff --git a/src/privacy_mode/win_virtual_display.rs b/src/privacy_mode/win_virtual_display.rs index 69fff6310039..7e322ebbf7e2 100644 --- a/src/privacy_mode/win_virtual_display.rs +++ b/src/privacy_mode/win_virtual_display.rs @@ -1,5 +1,5 @@ use super::{PrivacyMode, PrivacyModeState, INVALID_PRIVACY_MODE_CONN_ID, NO_PHYSICAL_DISPLAYS}; -use crate::virtual_display_manager; +use crate::{platform::windows::reg_display_settings, virtual_display_manager}; use hbb_common::{allow_err, bail, config::Config, log, ResultType}; use std::{ io::Error, @@ -150,7 +150,8 @@ impl PrivacyModeImpl { } fn restore_plug_out_monitor(&mut self) { - let _ = virtual_display_manager::plug_out_monitor_indices(&self.virtual_displays_added); + let _ = + virtual_display_manager::plug_out_monitor_indices(&self.virtual_displays_added, true); self.virtual_displays_added.clear(); } @@ -296,7 +297,7 @@ impl PrivacyModeImpl { // No physical displays, no need to use the privacy mode. if self.displays.is_empty() { - virtual_display_manager::plug_out_monitor_indices(&displays)?; + virtual_display_manager::plug_out_monitor_indices(&displays, false)?; bail!(NO_PHYSICAL_DISPLAYS); } @@ -414,8 +415,14 @@ impl PrivacyMode for PrivacyModeImpl { ) -> ResultType<()> { self.check_off_conn_id(conn_id)?; super::win_input::unhook()?; - self.restore_plug_out_monitor(); + let virtual_display_added = self.virtual_displays_added.len() > 0; + if virtual_display_added { + self.restore_plug_out_monitor(); + } restore_reg_connectivity(false); + if !virtual_display_added { + Self::commit_change_display(CDS_RESET)?; + } if self.conn_id != INVALID_PRIVACY_MODE_CONN_ID { if let Some(state) = state { @@ -462,7 +469,7 @@ pub fn restore_reg_connectivity(plug_out_monitors: bool) { return; } if plug_out_monitors { - let _ = virtual_display_manager::plug_out_monitor(-1); + let _ = virtual_display_manager::plug_out_monitor(-1, true); } if let Ok(reg_recovery) = serde_json::from_str::(&config_recovery_value) @@ -473,107 +480,3 @@ pub fn restore_reg_connectivity(plug_out_monitors: bool) { } reset_config_reg_connectivity(); } - -mod reg_display_settings { - use hbb_common::ResultType; - use serde_derive::{Deserialize, Serialize}; - use std::collections::HashMap; - use winreg::{enums::*, RegValue}; - const REG_GRAPHICS_DRIVERS_PATH: &str = "SYSTEM\\CurrentControlSet\\Control\\GraphicsDrivers"; - const REG_CONNECTIVITY_PATH: &str = "Connectivity"; - - #[derive(Serialize, Deserialize, Debug)] - pub(super) struct RegRecovery { - path: String, - key: String, - old: (Vec, isize), - new: (Vec, isize), - } - - pub(super) fn read_reg_connectivity() -> ResultType>> - { - let hklm = winreg::RegKey::predef(HKEY_LOCAL_MACHINE); - let reg_connectivity = hklm.open_subkey_with_flags( - format!("{}\\{}", REG_GRAPHICS_DRIVERS_PATH, REG_CONNECTIVITY_PATH), - KEY_READ, - )?; - - let mut map_connectivity = HashMap::new(); - for key in reg_connectivity.enum_keys() { - let key = key?; - let mut map_item = HashMap::new(); - let reg_item = reg_connectivity.open_subkey_with_flags(&key, KEY_READ)?; - for value in reg_item.enum_values() { - let (name, value) = value?; - map_item.insert(name, value); - } - map_connectivity.insert(key, map_item); - } - Ok(map_connectivity) - } - - pub(super) fn diff_recent_connectivity( - map1: HashMap>, - map2: HashMap>, - ) -> Option { - for (subkey, map_item2) in map2 { - if let Some(map_item1) = map1.get(&subkey) { - let key = "Recent"; - if let Some(value1) = map_item1.get(key) { - if let Some(value2) = map_item2.get(key) { - if value1 != value2 { - return Some(RegRecovery { - path: format!( - "{}\\{}\\{}", - REG_GRAPHICS_DRIVERS_PATH, REG_CONNECTIVITY_PATH, subkey - ), - key: key.to_owned(), - old: (value1.bytes.clone(), value1.vtype.clone() as isize), - new: (value2.bytes.clone(), value2.vtype.clone() as isize), - }); - } - } - } - } - } - None - } - - pub(super) fn restore_reg_connectivity(reg_recovery: RegRecovery) -> ResultType<()> { - let hklm = winreg::RegKey::predef(HKEY_LOCAL_MACHINE); - let reg_item = hklm.open_subkey_with_flags(®_recovery.path, KEY_READ | KEY_WRITE)?; - let cur_reg_value = reg_item.get_raw_value(®_recovery.key)?; - let new_reg_value = RegValue { - bytes: reg_recovery.new.0, - vtype: isize_to_reg_type(reg_recovery.new.1), - }; - if cur_reg_value != new_reg_value { - return Ok(()); - } - let reg_value = RegValue { - bytes: reg_recovery.old.0, - vtype: isize_to_reg_type(reg_recovery.old.1), - }; - reg_item.set_raw_value(®_recovery.key, ®_value)?; - Ok(()) - } - - #[inline] - fn isize_to_reg_type(i: isize) -> RegType { - match i { - 0 => RegType::REG_NONE, - 1 => RegType::REG_SZ, - 2 => RegType::REG_EXPAND_SZ, - 3 => RegType::REG_BINARY, - 4 => RegType::REG_DWORD, - 5 => RegType::REG_DWORD_BIG_ENDIAN, - 6 => RegType::REG_LINK, - 7 => RegType::REG_MULTI_SZ, - 8 => RegType::REG_RESOURCE_LIST, - 9 => RegType::REG_FULL_RESOURCE_DESCRIPTOR, - 10 => RegType::REG_RESOURCE_REQUIREMENTS_LIST, - 11 => RegType::REG_QWORD, - _ => RegType::REG_NONE, - } - } -} diff --git a/src/server/connection.rs b/src/server/connection.rs index b1af36e47c51..bc8c4ef0e34e 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -2670,7 +2670,7 @@ impl Connection { } } } else { - if let Err(e) = virtual_display_manager::plug_out_monitor(t.display) { + if let Err(e) = virtual_display_manager::plug_out_monitor(t.display, false) { log::error!("Failed to plug out virtual display {}: {}", t.display, e); self.send(make_msg(format!( "Failed to plug out virtual displays: {}", diff --git a/src/server/display_service.rs b/src/server/display_service.rs index b4abdecfa4a5..7260b9c7a23b 100644 --- a/src/server/display_service.rs +++ b/src/server/display_service.rs @@ -433,7 +433,6 @@ pub fn try_get_displays_(add_amyuni_headless: bool) -> ResultType> // } let no_displays_v = no_displays(&displays); - virtual_display_manager::set_can_plug_out_all(!no_displays_v); if no_displays_v { log::debug!("no displays, create virtual display"); if let Err(e) = virtual_display_manager::plug_in_headless() { diff --git a/src/virtual_display_manager.rs b/src/virtual_display_manager.rs index 7807bd3d548f..138087c75e86 100644 --- a/src/virtual_display_manager.rs +++ b/src/virtual_display_manager.rs @@ -1,5 +1,4 @@ use hbb_common::{bail, platform::windows::is_windows_version_or_greater, ResultType}; -use std::sync::atomic; // This string is defined here. // https://github.com/rustdesk-org/RustDeskIddDriver/blob/b370aad3f50028b039aad211df60c8051c4a64d6/RustDeskIddDriver/RustDeskIddDriver.inf#LL73C1-L73C40 @@ -10,29 +9,6 @@ const IDD_IMPL: &str = IDD_IMPL_AMYUNI; const IDD_IMPL_RUSTDESK: &str = "rustdesk_idd"; const IDD_IMPL_AMYUNI: &str = "amyuni_idd"; -const IS_CAN_PLUG_OUT_ALL_NOT_SET: i8 = 0; -const IS_CAN_PLUG_OUT_ALL_YES: i8 = 1; -const IS_CAN_PLUG_OUT_ALL_NO: i8 = 2; -static IS_CAN_PLUG_OUT_ALL: atomic::AtomicI8 = atomic::AtomicI8::new(IS_CAN_PLUG_OUT_ALL_NOT_SET); - -pub fn is_can_plug_out_all() -> bool { - IS_CAN_PLUG_OUT_ALL.load(atomic::Ordering::Relaxed) != IS_CAN_PLUG_OUT_ALL_NO -} - -// No need to consider concurrency here. -pub fn set_can_plug_out_all(v: bool) { - if IS_CAN_PLUG_OUT_ALL.load(atomic::Ordering::Relaxed) == IS_CAN_PLUG_OUT_ALL_NOT_SET { - IS_CAN_PLUG_OUT_ALL.store( - if v { - IS_CAN_PLUG_OUT_ALL_YES - } else { - IS_CAN_PLUG_OUT_ALL_NO - }, - atomic::Ordering::Relaxed, - ); - } -} - pub fn is_amyuni_idd() -> bool { IDD_IMPL == IDD_IMPL_AMYUNI } @@ -100,7 +76,7 @@ pub fn plug_in_monitor(idx: u32, modes: Vec) -> Re } } -pub fn plug_out_monitor(index: i32) -> ResultType<()> { +pub fn plug_out_monitor(index: i32, force_all: bool) -> ResultType<()> { match IDD_IMPL { IDD_IMPL_RUSTDESK => { let indices = if index == -1 { @@ -110,7 +86,7 @@ pub fn plug_out_monitor(index: i32) -> ResultType<()> { }; rustdesk_idd::plug_out_peer_request(&indices) } - IDD_IMPL_AMYUNI => amyuni_idd::plug_out_monitor(index), + IDD_IMPL_AMYUNI => amyuni_idd::plug_out_monitor(index, force_all), _ => bail!("Unsupported virtual display implementation."), } } @@ -126,12 +102,12 @@ pub fn plug_in_peer_request(modes: Vec>) -> Re } } -pub fn plug_out_monitor_indices(indices: &[u32]) -> ResultType<()> { +pub fn plug_out_monitor_indices(indices: &[u32], force_all: bool) -> ResultType<()> { match IDD_IMPL { IDD_IMPL_RUSTDESK => rustdesk_idd::plug_out_peer_request(indices), IDD_IMPL_AMYUNI => { for _idx in indices.iter() { - amyuni_idd::plug_out_monitor(0)?; + amyuni_idd::plug_out_monitor(0, force_all)?; } Ok(()) } @@ -142,7 +118,7 @@ pub fn plug_out_monitor_indices(indices: &[u32]) -> ResultType<()> { pub fn reset_all() -> ResultType<()> { match IDD_IMPL { IDD_IMPL_RUSTDESK => rustdesk_idd::reset_all(), - IDD_IMPL_AMYUNI => crate::privacy_mode::turn_off_privacy(0, None).unwrap_or(Ok(())), + IDD_IMPL_AMYUNI => amyuni_idd::reset_all(), _ => bail!("Unsupported virtual display implementation."), } } @@ -402,7 +378,7 @@ pub mod rustdesk_idd { pub mod amyuni_idd { use super::windows; - use crate::platform::win_device; + use crate::platform::{reg_display_settings, win_device}; use hbb_common::{bail, lazy_static, log, tokio::time::Instant, ResultType}; use std::{ ptr::null_mut, @@ -532,6 +508,13 @@ pub mod amyuni_idd { Ok(()) } + pub fn reset_all() -> ResultType<()> { + let _ = crate::privacy_mode::turn_off_privacy(0, None); + let _ = plug_out_monitor(-1, true); + *LAST_PLUG_IN_HEADLESS_TIME.lock().unwrap() = None; + Ok(()) + } + #[inline] fn plug_monitor_(add: bool) -> Result<(), win_device::DeviceError> { let cmd = if add { 0x10 } else { 0x00 }; @@ -547,6 +530,7 @@ pub mod amyuni_idd { fn plug_in_monitor_(add: bool, is_driver_async_installed: bool) -> ResultType<()> { let timeout = Duration::from_secs(3); let now = Instant::now(); + let reg_connectivity_old = reg_display_settings::read_reg_connectivity(); loop { match plug_monitor_(add) { Ok(_) => { @@ -567,9 +551,36 @@ pub mod amyuni_idd { } } } + // Workaround for the issue that we can't set the default the resolution. + if let Ok(old_connectivity_old) = reg_connectivity_old { + std::thread::spawn(move || { + try_reset_resolution_on_first_plug_in(old_connectivity_old.len(), 1920, 1080); + }); + } + Ok(()) } + fn try_reset_resolution_on_first_plug_in( + old_connectivity_len: usize, + width: usize, + height: usize, + ) { + for _ in 0..10 { + std::thread::sleep(Duration::from_millis(300)); + if let Ok(reg_connectivity_new) = reg_display_settings::read_reg_connectivity() { + if reg_connectivity_new.len() != old_connectivity_len { + for name in + windows::get_device_names(Some(super::AMYUNI_IDD_DEVICE_STRING)).iter() + { + crate::platform::change_resolution(&name, width, height).ok(); + } + break; + } + } + } + } + pub fn plug_in_headless() -> ResultType<()> { let mut tm = LAST_PLUG_IN_HEADLESS_TIME.lock().unwrap(); if let Some(tm) = &mut *tm { @@ -603,7 +614,7 @@ pub mod amyuni_idd { plug_in_monitor_(true, is_async) } - pub fn plug_out_monitor(index: i32) -> ResultType<()> { + pub fn plug_out_monitor(index: i32, force_all: bool) -> ResultType<()> { let all_count = windows::get_device_names(None).len(); let amyuni_count = get_monitor_count(); let mut to_plug_out_count = match all_count { @@ -612,7 +623,7 @@ pub mod amyuni_idd { if amyuni_count == 0 { bail!("No virtual displays to plug out.") } else { - if super::is_can_plug_out_all() { + if force_all { 1 } else { bail!("This only virtual display cannot be pulled out.") @@ -621,7 +632,7 @@ pub mod amyuni_idd { } _ => { if all_count == amyuni_count { - if super::is_can_plug_out_all() { + if force_all { all_count } else { all_count - 1 From 99d7b62d792e1b85145c51ba7312b970fc517f9a Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Sun, 11 Aug 2024 23:28:02 +0800 Subject: [PATCH 084/541] fix: privacy mode 2, resolution changed (#9027) Signed-off-by: fufesou --- src/server/video_service.rs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/server/video_service.rs b/src/server/video_service.rs index 7fd5dee1f82a..8b326a2ffdd3 100644 --- a/src/server/video_service.rs +++ b/src/server/video_service.rs @@ -540,7 +540,7 @@ fn run(vs: VideoService) -> ResultType<()> { VRamEncoder::set_fallback_gdi(display_idx, true); bail!("SWITCH"); } - check_privacy_mode_changed(&sp, c.privacy_mode_id)?; + check_privacy_mode_changed(&sp, display_idx, &c)?; #[cfg(windows)] { if crate::platform::windows::desktop_changed() @@ -659,7 +659,7 @@ fn run(vs: VideoService) -> ResultType<()> { let timeout_millis = 3_000u64; let wait_begin = Instant::now(); while wait_begin.elapsed().as_millis() < timeout_millis as _ { - check_privacy_mode_changed(&sp, c.privacy_mode_id)?; + check_privacy_mode_changed(&sp, display_idx, &c)?; frame_controller.try_wait_next(&mut fetched_conn_ids, 300); // break if all connections have received current frame if fetched_conn_ids.len() >= frame_controller.send_conn_ids.len() { @@ -876,9 +876,13 @@ fn check_change_scale(hardware: bool) -> ResultType<()> { Ok(()) } -fn check_privacy_mode_changed(sp: &GenericService, privacy_mode_id: i32) -> ResultType<()> { +fn check_privacy_mode_changed( + sp: &GenericService, + display_idx: usize, + ci: &CapturerInfo, +) -> ResultType<()> { let privacy_mode_id_2 = get_privacy_mode_conn_id().unwrap_or(INVALID_PRIVACY_MODE_CONN_ID); - if privacy_mode_id != privacy_mode_id_2 { + if ci.privacy_mode_id != privacy_mode_id_2 { if privacy_mode_id_2 != INVALID_PRIVACY_MODE_CONN_ID { let msg_out = crate::common::make_privacy_mode_msg( back_notification::PrivacyModeState::PrvOnByOther, @@ -887,6 +891,7 @@ fn check_privacy_mode_changed(sp: &GenericService, privacy_mode_id: i32) -> Resu sp.send_to_others(msg_out, privacy_mode_id_2); } log::info!("switch due to privacy mode changed"); + try_broadcast_display_changed(&sp, display_idx, ci, true).ok(); bail!("SWITCH"); } Ok(()) From 57834840b8ffab75dcf553f2904f8b14f1152105 Mon Sep 17 00:00:00 2001 From: Xp96 <38923106+Xp96@users.noreply.github.com> Date: Mon, 12 Aug 2024 06:22:00 -0300 Subject: [PATCH 085/541] Update ptbr.rs (#9031) --- src/lang/ptbr.rs | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index b3804bd5c58d..7bf00d20734d 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -559,21 +559,21 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Big tiles", ""), ("Small tiles", ""), ("List", "Lista"), - ("Virtual display", ""), + ("Virtual display", "Display Virtual"), ("Plug out all", ""), ("True color (4:4:4)", ""), ("Enable blocking user input", ""), ("id_input_tip", ""), ("privacy_mode_impl_mag_tip", ""), ("privacy_mode_impl_virtual_display_tip", ""), - ("Enter privacy mode", ""), - ("Exit privacy mode", ""), + ("Enter privacy mode", "Entrar no modo privado"), + ("Exit privacy mode", "Sair do modo privado"), ("idd_not_support_under_win10_2004_tip", ""), ("input_source_1_tip", ""), ("input_source_2_tip", ""), ("Swap control-command key", ""), ("swap-left-right-mouse", ""), - ("2FA code", ""), + ("2FA code", "Código 2FA"), ("More", ""), ("enable-2fa-title", ""), ("enable-2fa-desc", ""), @@ -619,22 +619,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("During controlled", ""), ("During service is on", ""), ("Capture screen using DirectX", ""), - ("Back", ""), + ("Back", "Voltar"), ("Apps", ""), ("Volume up", ""), ("Volume down", ""), ("Power", ""), - ("Telegram bot", ""), - ("enable-bot-tip", ""), - ("enable-bot-desc", ""), - ("cancel-2fa-confirm-tip", ""), - ("cancel-bot-confirm-tip", ""), - ("About RustDesk", ""), + ("Telegram bot", "Bot Telegram"), + ("enable-bot-tip", "Se você ativar este recurso, poderá receber o código 2FA do seu bot. Ele também pode funcionar como uma notificação de conexão."), + ("enable-bot-desc", "1. Abra um chat com @BotFather.\n2. Envie o comando \"/newbot\". Você receberá um token após completar esta etapa.\n3. Inicie um chat com o seu bot recém-criado. Envie uma mensagem começando com uma barra invertida (\"/\"), como \"/hello\", para ativá-lo.\n"), + ("cancel-2fa-confirm-tip", "Tem certeza de que deseja cancelar a 2FA?"), + ("cancel-bot-confirm-tip", "Tem certeza de que deseja cancelar o bot do Telegram?"), + ("About RustDesk", "Sobre RustDesk"), ("Send clipboard keystrokes", ""), - ("network_error_tip", ""), - ("Unlock with PIN", ""), + ("network_error_tip", "Por favor, verifique sua conexão de rede e clique em tentar novamente."), + ("Unlock with PIN", "Desbloquear com PIN"), ("Requires at least {} characters", ""), - ("Wrong PIN", ""), - ("Set PIN", ""), + ("Wrong PIN", "PIN Errado"), + ("Set PIN", "Definir PIN"), ].iter().cloned().collect(); } From 1729ee337f22145607bea1b2b9f68692ec661a5a Mon Sep 17 00:00:00 2001 From: 21pages Date: Mon, 12 Aug 2024 18:08:33 +0800 Subject: [PATCH 086/541] trust this device to skip 2fa (#9012) * trust this device to skip 2fa Signed-off-by: 21pages * Update connection.rs --------- Signed-off-by: 21pages Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com> --- flutter/lib/common/widgets/dialog.dart | 178 +++++++++++++++++- flutter/lib/consts.dart | 1 + .../desktop/pages/desktop_setting_page.dart | 27 ++- flutter/lib/mobile/pages/settings_page.dart | 100 +++++++++- flutter/lib/models/model.dart | 5 +- flutter/lib/web/bridge.dart | 22 ++- libs/hbb_common/protos/message.proto | 3 + libs/hbb_common/src/config.rs | 81 ++++++++ src/auth_2fa.rs | 6 +- src/client.rs | 13 ++ src/client/io_loop.rs | 4 + src/common.rs | 9 + src/flutter_ffi.rs | 25 ++- src/ipc.rs | 49 ++++- src/lang/ar.rs | 6 + src/lang/be.rs | 6 + src/lang/bg.rs | 6 + src/lang/ca.rs | 6 + src/lang/cn.rs | 6 + src/lang/cs.rs | 6 + src/lang/da.rs | 6 + src/lang/de.rs | 6 + src/lang/el.rs | 6 + src/lang/en.rs | 3 +- src/lang/eo.rs | 6 + src/lang/es.rs | 6 + src/lang/et.rs | 6 + src/lang/eu.rs | 6 + src/lang/fa.rs | 6 + src/lang/fr.rs | 6 + src/lang/he.rs | 6 + src/lang/hr.rs | 6 + src/lang/hu.rs | 6 + src/lang/id.rs | 6 + src/lang/it.rs | 6 + src/lang/ja.rs | 6 + src/lang/ko.rs | 6 + src/lang/kz.rs | 6 + src/lang/lt.rs | 6 + src/lang/lv.rs | 6 + src/lang/nb.rs | 6 + src/lang/nl.rs | 6 + src/lang/pl.rs | 6 + src/lang/pt_PT.rs | 6 + src/lang/ptbr.rs | 6 + src/lang/ro.rs | 6 + src/lang/ru.rs | 6 + src/lang/sk.rs | 6 + src/lang/sl.rs | 6 + src/lang/sq.rs | 6 + src/lang/sr.rs | 6 + src/lang/sv.rs | 6 + src/lang/template.rs | 6 + src/lang/th.rs | 6 + src/lang/tr.rs | 6 + src/lang/tw.rs | 6 + src/lang/uk.rs | 6 + src/lang/vn.rs | 6 + src/server/connection.rs | 35 +++- src/ui/common.tis | 2 +- src/ui/msgbox.tis | 2 + src/ui/remote.rs | 3 +- src/ui_interface.rs | 25 +++ src/ui_session_interface.rs | 16 +- 64 files changed, 845 insertions(+), 22 deletions(-) diff --git a/flutter/lib/common/widgets/dialog.dart b/flutter/lib/common/widgets/dialog.dart index 58f85feb09e8..7cc76d6c6a1b 100644 --- a/flutter/lib/common/widgets/dialog.dart +++ b/flutter/lib/common/widgets/dialog.dart @@ -1831,6 +1831,7 @@ void changeBot({Function()? callback}) async { void change2fa({Function()? callback}) async { if (bind.mainHasValid2FaSync()) { await bind.mainSetOption(key: "2fa", value: ""); + await bind.mainClearTrustedDevices(); callback?.call(); return; } @@ -1898,6 +1899,7 @@ void enter2FaDialog( SessionID sessionId, OverlayDialogManager dialogManager) async { final controller = TextEditingController(); final RxBool submitReady = false.obs; + final RxBool trustThisDevice = false.obs; dialogManager.dismissAll(); dialogManager.show((setState, close, context) { @@ -1907,7 +1909,7 @@ void enter2FaDialog( } submit() { - gFFI.send2FA(sessionId, controller.text.trim()); + gFFI.send2FA(sessionId, controller.text.trim(), trustThisDevice.value); close(); dialogManager.showLoading(translate('Logging in...'), onCancel: closeConnection); @@ -1921,9 +1923,27 @@ void enter2FaDialog( 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( title: Text(translate('enter-2fa-title')), - content: codeField, + content: Column( + children: [ + codeField, + if (bind.sessionGetEnableTrustedDevices(sessionId: sessionId)) + trustField, + ], + ), actions: [ dialogButton('Cancel', onPressed: cancel, @@ -2313,3 +2333,157 @@ void checkUnlockPinDialog(String correctPin, Function() passCallback) { ); }); } + +void confrimDeleteTrustedDevicesDialog( + RxList trustedDevices, RxList 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 trustedDevices = (await TrustedDevice.get()).obs; + RxList 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 json) { + final hwidList = json['hwid'] as List; + hwid = Uint8List.fromList(hwidList.cast()); + 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> get() async { + final List 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 devices, RxList 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(), + )), + ); +} diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index 5af3f6d251fd..b836200a4347 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -136,6 +136,7 @@ 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"; diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index a016a61ae3c0..6d9f92b59dc4 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -783,8 +783,33 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { onChangedBot(!hasBot.value); }, ).marginOnly(left: _kCheckBoxLeftMargin + 30); + + final trust = Row( + children: [ + Flexible( + child: Tooltip( + waitDuration: Duration(milliseconds: 300), + message: translate("enable-trusted-devices-tip"), + child: _OptionCheckBox(context, "Enable trusted devices", + kOptionEnableTrustedDevices, + enabled: !locked, update: (v) { + setState(() {}); + }), + ), + ), + if (mainGetBoolOptionSync(kOptionEnableTrustedDevices)) + ElevatedButton( + onPressed: locked + ? null + : () { + manageTrustedDeviceDialog(); + }, + child: Text(translate('Manage trusted devices'))) + ], + ).marginOnly(left: 30); + return Column( - children: [tfa, bot], + children: [tfa, bot, trust], ); } diff --git a/flutter/lib/mobile/pages/settings_page.dart b/flutter/lib/mobile/pages/settings_page.dart index c817fda4e514..98a2b9f50c1b 100644 --- a/flutter/lib/mobile/pages/settings_page.dart +++ b/flutter/lib/mobile/pages/settings_page.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:convert'; +import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/common/widgets/setting_widgets.dart'; @@ -87,6 +88,7 @@ class _SettingsState extends State with WidgetsBindingObserver { var _hideServer = false; var _hideProxy = false; var _hideNetwork = false; + var _enableTrustedDevices = false; _SettingsState() { _enableAbr = option2bool( @@ -113,6 +115,7 @@ class _SettingsState extends State with WidgetsBindingObserver { _hideProxy = bind.mainGetBuildinOption(key: kOptionHideProxySetting) == 'Y'; _hideNetwork = bind.mainGetBuildinOption(key: kOptionHideNetworkSetting) == 'Y'; + _enableTrustedDevices = mainGetBoolOptionSync(kOptionEnableTrustedDevices); } @override @@ -243,18 +246,57 @@ class _SettingsState extends State with WidgetsBindingObserver { ], )); final List enhancementsTiles = []; - final List shareScreenTiles = [ + final enable2fa = bind.mainHasValid2FaSync(); + final List tfaTiles = [ SettingsTile.switchTile( title: Text(translate('enable-2fa-title')), - initialValue: bind.mainHasValid2FaSync(), - onToggle: (_) async { + initialValue: enable2fa, + onToggle: (v) async { update() async { setState(() {}); } - change2fa(callback: update); + if (v == false) { + CommonConfirmDialog( + gFFI.dialogManager, translate('cancel-2fa-confirm-tip'), () { + change2fa(callback: update); + }); + } else { + change2fa(callback: update); + } }, ), + if (enable2fa) + SettingsTile.switchTile( + title: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(translate('Enable trusted devices')), + Text(translate('enable-trusted-devices-tip'), + style: Theme.of(context).textTheme.bodySmall), + ], + ), + initialValue: _enableTrustedDevices, + onToggle: isOptionFixed(kOptionEnableTrustedDevices) + ? null + : (v) async { + mainSetBoolOption(kOptionEnableTrustedDevices, v); + setState(() { + _enableTrustedDevices = v; + }); + }, + ), + if (enable2fa && _enableTrustedDevices) + SettingsTile( + title: Text(translate('Manage trusted devices')), + trailing: Icon(Icons.arrow_forward_ios), + onPressed: (context) { + Navigator.push(context, MaterialPageRoute(builder: (context) { + return _ManageTrustedDevices(); + })); + }) + ]; + final List shareScreenTiles = [ SettingsTile.switchTile( title: Text(translate('Deny LAN discovery')), initialValue: _denyLANDiscovery, @@ -642,6 +684,11 @@ class _SettingsState extends State with WidgetsBindingObserver { ), ], ), + if (isAndroid && + !disabledSettings && + !outgoingOnly && + !hideSecuritySettings) + SettingsSection(title: Text('2FA'), tiles: tfaTiles), if (isAndroid && !disabledSettings && !outgoingOnly && @@ -963,6 +1010,51 @@ class __DisplayPageState extends State<_DisplayPage> { } } +class _ManageTrustedDevices extends StatefulWidget { + const _ManageTrustedDevices(); + + @override + State<_ManageTrustedDevices> createState() => __ManageTrustedDevicesState(); +} + +class __ManageTrustedDevicesState extends State<_ManageTrustedDevices> { + RxList trustedDevices = RxList.empty(growable: true); + RxList selectedDevices = RxList.empty(); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(translate('Manage trusted devices')), + centerTitle: true, + actions: [ + Obx(() => IconButton( + icon: Icon(Icons.delete, color: Colors.white), + onPressed: selectedDevices.isEmpty + ? null + : () { + confrimDeleteTrustedDevicesDialog( + trustedDevices, selectedDevices); + })) + ], + ), + body: FutureBuilder( + future: TrustedDevice.get(), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return Center(child: CircularProgressIndicator()); + } + if (snapshot.hasError) { + return Center(child: Text('Error: ${snapshot.error}')); + } + final devices = snapshot.data as List; + trustedDevices = devices.obs; + return trustedDevicesTable(trustedDevices, selectedDevices); + }), + ); + } +} + class _RadioEntry { final String label; final String value; diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 6a900902617d..050a92a5f641 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -2611,8 +2611,9 @@ class FFI { remember: remember); } - void send2FA(SessionID sessionId, String code) { - bind.sessionSend2Fa(sessionId: sessionId, code: code); + void send2FA(SessionID sessionId, String code, bool trustThisDevice) { + bind.sessionSend2Fa( + sessionId: sessionId, code: code, trustThisDevice: trustThisDevice); } /// Close the remote session. diff --git a/flutter/lib/web/bridge.dart b/flutter/lib/web/bridge.dart index 7795cb82468b..3f3846e08811 100644 --- a/flutter/lib/web/bridge.dart +++ b/flutter/lib/web/bridge.dart @@ -142,7 +142,10 @@ class RustdeskImpl { } Future sessionSend2Fa( - {required UuidValue sessionId, required String code, dynamic hint}) { + {required UuidValue sessionId, + required String code, + required bool trustThisDevice, + dynamic hint}) { return Future(() => js.context.callMethod('setByName', ['send_2fa', code])); } @@ -1630,5 +1633,22 @@ class RustdeskImpl { throw UnimplementedError(); } + bool sessionGetEnableTrustedDevices( + {required UuidValue sessionId, dynamic hint}) { + throw UnimplementedError(); + } + + Future mainGetTrustedDevices({dynamic hint}) { + throw UnimplementedError(); + } + + Future mainRemoveTrustedDevices({required String json, dynamic hint}) { + throw UnimplementedError(); + } + + Future mainClearTrustedDevices({dynamic hint}) { + throw UnimplementedError(); + } + void dispose() {} } diff --git a/libs/hbb_common/protos/message.proto b/libs/hbb_common/protos/message.proto index d7a8cf0a7cd6..497bdee9ba8b 100644 --- a/libs/hbb_common/protos/message.proto +++ b/libs/hbb_common/protos/message.proto @@ -82,10 +82,12 @@ message LoginRequest { string version = 11; OSLogin os_login = 12; string my_platform = 13; + bytes hwid = 14; } message Auth2FA { string code = 1; + bytes hwid = 2; } message ChatMessage { string text = 1; } @@ -137,6 +139,7 @@ message LoginResponse { string error = 1; PeerInfo peer_info = 2; } + bool enable_trusted_devices = 3; } message TouchScaleUpdate { diff --git a/libs/hbb_common/src/config.rs b/libs/hbb_common/src/config.rs index 2dfa88b9652b..d0b908b551c2 100644 --- a/libs/hbb_common/src/config.rs +++ b/libs/hbb_common/src/config.rs @@ -10,6 +10,7 @@ use std::{ }; use anyhow::Result; +use bytes::Bytes; use rand::Rng; use regex::Regex; use serde as de; @@ -52,6 +53,7 @@ lazy_static::lazy_static! { static ref CONFIG: RwLock = RwLock::new(Config::load()); static ref CONFIG2: RwLock = RwLock::new(Config2::load()); static ref LOCAL_CONFIG: RwLock = RwLock::new(LocalConfig::load()); + static ref TRUSTED_DEVICES: RwLock<(Vec, bool)> = Default::default(); static ref ONLINE: Mutex> = Default::default(); pub static ref PROD_RENDEZVOUS_SERVER: RwLock = RwLock::new(match option_env!("RENDEZVOUS_SERVER") { Some(key) if !key.is_empty() => key, @@ -210,6 +212,8 @@ pub struct Config2 { serial: i32, #[serde(default, deserialize_with = "deserialize_string")] unlock_pin: String, + #[serde(default, deserialize_with = "deserialize_string")] + trusted_devices: String, #[serde(default)] socks: Option, @@ -998,6 +1002,7 @@ impl Config { } config.password = password.into(); config.store(); + Self::clear_trusted_devices(); } pub fn get_permanent_password() -> String { @@ -1104,6 +1109,64 @@ impl Config { config.store(); } + pub fn get_trusted_devices_json() -> String { + serde_json::to_string(&Self::get_trusted_devices()).unwrap_or_default() + } + + pub fn get_trusted_devices() -> Vec { + let (devices, synced) = TRUSTED_DEVICES.read().unwrap().clone(); + if synced { + return devices; + } + let devices = CONFIG2.read().unwrap().trusted_devices.clone(); + let (devices, succ, store) = decrypt_str_or_original(&devices, PASSWORD_ENC_VERSION); + if succ { + let mut devices: Vec = + serde_json::from_str(&devices).unwrap_or_default(); + let len = devices.len(); + devices.retain(|d| !d.outdate()); + if store || devices.len() != len { + Self::set_trusted_devices(devices.clone()); + } + *TRUSTED_DEVICES.write().unwrap() = (devices.clone(), true); + devices + } else { + Default::default() + } + } + + fn set_trusted_devices(mut trusted_devices: Vec) { + trusted_devices.retain(|d| !d.outdate()); + let devices = serde_json::to_string(&trusted_devices).unwrap_or_default(); + let max_len = 1024 * 1024; + if devices.bytes().len() > max_len { + log::error!("Trusted devices too large: {}", devices.bytes().len()); + return; + } + let devices = encrypt_str_or_original(&devices, PASSWORD_ENC_VERSION, max_len); + let mut config = CONFIG2.write().unwrap(); + config.trusted_devices = devices; + config.store(); + *TRUSTED_DEVICES.write().unwrap() = (trusted_devices, true); + } + + pub fn add_trusted_device(device: TrustedDevice) { + let mut devices = Self::get_trusted_devices(); + devices.retain(|d| d.hwid != device.hwid); + devices.push(device); + Self::set_trusted_devices(devices); + } + + pub fn remove_trusted_devices(hwids: &Vec) { + let mut devices = Self::get_trusted_devices(); + devices.retain(|d| !hwids.contains(&d.hwid)); + Self::set_trusted_devices(devices); + } + + pub fn clear_trusted_devices() { + Self::set_trusted_devices(Default::default()); + } + pub fn get() -> Config { return CONFIG.read().unwrap().clone(); } @@ -1934,6 +1997,22 @@ impl Group { } } +#[derive(Debug, Default, Serialize, Deserialize, Clone)] +pub struct TrustedDevice { + pub hwid: Bytes, + pub time: i64, + pub id: String, + pub name: String, + pub platform: String, +} + +impl TrustedDevice { + pub fn outdate(&self) -> bool { + const DAYS_90: i64 = 90 * 24 * 60 * 60 * 1000; + self.time + DAYS_90 < crate::get_time() + } +} + deserialize_default!(deserialize_string, String); deserialize_default!(deserialize_bool, bool); deserialize_default!(deserialize_i32, i32); @@ -2123,6 +2202,7 @@ pub mod keys { pub const OPTION_ENABLE_DIRECTX_CAPTURE: &str = "enable-directx-capture"; pub const OPTION_ENABLE_ANDROID_SOFTWARE_ENCODING_HALF_SCALE: &str = "enable-android-software-encoding-half-scale"; + pub const OPTION_ENABLE_TRUSTED_DEVICES: &str = "enable-trusted-devices"; // buildin options pub const OPTION_DISPLAY_NAME: &str = "display-name"; @@ -2264,6 +2344,7 @@ pub mod keys { OPTION_PRESET_ADDRESS_BOOK_TAG, OPTION_ENABLE_DIRECTX_CAPTURE, OPTION_ENABLE_ANDROID_SOFTWARE_ENCODING_HALF_SCALE, + OPTION_ENABLE_TRUSTED_DEVICES, ]; // BUILDIN_SETTINGS diff --git a/src/auth_2fa.rs b/src/auth_2fa.rs index 6945bf461932..1c243bc77646 100644 --- a/src/auth_2fa.rs +++ b/src/auth_2fa.rs @@ -4,7 +4,7 @@ use hbb_common::{ config::Config, get_time, password_security::{decrypt_vec_or_original, encrypt_vec_or_original}, - tokio, ResultType, + ResultType, }; use serde_derive::{Deserialize, Serialize}; use std::sync::Mutex; @@ -165,9 +165,7 @@ pub async fn send_2fa_code_to_telegram(text: &str, bot: TelegramBot) -> ResultTy pub fn get_chatid_telegram(bot_token: &str) -> ResultType> { let url = format!("https://api.telegram.org/bot{}/getUpdates", bot_token); // because caller is in tokio runtime, so we must call post_request_sync in new thread. - let handle = std::thread::spawn(move || { - crate::post_request_sync(url, "".to_owned(), "") - }); + let handle = std::thread::spawn(move || crate::post_request_sync(url, "".to_owned(), "")); let resp = handle.join().map_err(|_| anyhow!("Thread panicked"))??; let value = serde_json::from_str::(&resp).map_err(|e| anyhow!(e))?; diff --git a/src/client.rs b/src/client.rs index ec85f4807467..2c5d0a3399e9 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1329,6 +1329,7 @@ pub struct LoginConfigHandler { pub peer_info: Option, password_source: PasswordSource, // where the sent password comes from shared_password: Option, // Store the shared password + pub enable_trusted_devices: bool, } impl Deref for LoginConfigHandler { @@ -2156,6 +2157,11 @@ impl LoginConfigHandler { let my_platform = whoami::platform().to_string(); #[cfg(target_os = "android")] let my_platform = "Android".into(); + let hwid = if self.get_option("trust-this-device") == "Y" { + crate::get_hwid() + } else { + Bytes::new() + }; let mut lr = LoginRequest { username: pure_id, password: password.into(), @@ -2171,6 +2177,7 @@ impl LoginConfigHandler { ..Default::default() }) .into(), + hwid, ..Default::default() }; match self.conn_type { @@ -2827,6 +2834,12 @@ pub fn handle_login_error( interface.msgbox("re-input-password", err, "Do you want to enter again?", ""); true } else if err == LOGIN_MSG_2FA_WRONG || err == REQUIRE_2FA { + let enabled = lc.read().unwrap().get_option("trust-this-device") == "Y"; + if enabled { + lc.write() + .unwrap() + .set_option("trust-this-device".to_string(), "".to_string()); + } interface.msgbox("input-2fa", err, "", ""); true } else if LOGIN_ERROR_MAP.contains_key(err) { diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index ce32b0d26899..537de56add1b 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -1135,6 +1135,10 @@ impl Remote { } Some(message::Union::LoginResponse(lr)) => match lr.union { Some(login_response::Union::Error(err)) => { + if err == client::REQUIRE_2FA { + self.handler.lc.write().unwrap().enable_trusted_devices = + lr.enable_trusted_devices; + } if !self.handler.handle_login_error(&err) { return false; } diff --git a/src/common.rs b/src/common.rs index 108d25659f27..1ed9d6a8fb40 100644 --- a/src/common.rs +++ b/src/common.rs @@ -1494,6 +1494,15 @@ pub fn is_empty_uni_link(arg: &str) -> bool { arg[prefix.len()..].chars().all(|c| c == '/') } +pub fn get_hwid() -> Bytes { + use sha2::{Digest, Sha256}; + + let uuid = hbb_common::get_uuid(); + let mut hasher = Sha256::new(); + hasher.update(&uuid); + Bytes::from(hasher.finalize().to_vec()) +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index d64823b8dc6a..60f62e102bf3 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -208,12 +208,21 @@ pub fn session_login( } } -pub fn session_send2fa(session_id: SessionID, code: String) { +pub fn session_send2fa(session_id: SessionID, code: String, trust_this_device: bool) { if let Some(session) = sessions::get_session_by_session_id(&session_id) { - session.send2fa(code); + session.send2fa(code, trust_this_device); } } +pub fn session_get_enable_trusted_devices(session_id: SessionID) -> SyncReturn { + let v = if let Some(session) = sessions::get_session_by_session_id(&session_id) { + session.get_enable_trusted_devices() + } else { + false + }; + SyncReturn(v) +} + pub fn session_close(session_id: SessionID) { if let Some(session) = sessions::remove_session_by_session_id(&session_id) { session.close_event_stream(session_id); @@ -2240,6 +2249,18 @@ pub fn main_check_hwcodec() { check_hwcodec() } +pub fn main_get_trusted_devices() -> String { + get_trusted_devices() +} + +pub fn main_remove_trusted_devices(json: String) { + remove_trusted_devices(&json) +} + +pub fn main_clear_trusted_devices() { + clear_trusted_devices() +} + pub fn session_request_new_display_init_msgs(session_id: SessionID, display: usize) { if let Some(session) = sessions::get_session_by_session_id(&session_id) { session.request_init_msgs(display); diff --git a/src/ipc.rs b/src/ipc.rs index be869488b4f3..7903a942b4f5 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -25,7 +25,9 @@ use hbb_common::{ config::{self, Config, Config2}, futures::StreamExt as _, futures_util::sink::SinkExt, - log, password_security as password, timeout, + log, password_security as password, + sodiumoxide::base64, + timeout, tokio::{ self, io::{AsyncRead, AsyncWrite}, @@ -260,6 +262,8 @@ pub enum Data { // Although the key is not neccessary, it is used to avoid hardcoding the key. WaylandScreencastRestoreToken((String, String)), HwCodecConfig(Option), + RemoveTrustedDevices(Vec), + ClearTrustedDevices, } #[tokio::main(flavor = "current_thread")] @@ -486,6 +490,8 @@ async fn handle(data: Data, stream: &mut Connection) { value = crate::audio_service::get_voice_call_input_device(); } else if name == "unlock-pin" { value = Some(Config::get_unlock_pin()); + } else if name == "trusted-devices" { + value = Some(Config::get_trusted_devices_json()); } else { value = None; } @@ -638,6 +644,12 @@ async fn handle(data: Data, stream: &mut Connection) { ); } } + Data::RemoveTrustedDevices(v) => { + Config::remove_trusted_devices(&v); + } + Data::ClearTrustedDevices => { + Config::clear_trusted_devices(); + } _ => {} } } @@ -866,6 +878,17 @@ pub async fn set_config_async(name: &str, value: String) -> ResultType<()> { Ok(()) } +#[tokio::main(flavor = "current_thread")] +pub async fn set_data(data: &Data) -> ResultType<()> { + set_data_async(data).await +} + +pub async fn set_data_async(data: &Data) -> ResultType<()> { + let mut c = connect(1000, "").await?; + c.send(data).await?; + Ok(()) +} + #[tokio::main(flavor = "current_thread")] pub async fn set_config(name: &str, value: String) -> ResultType<()> { set_config_async(name, value).await @@ -926,6 +949,30 @@ pub fn get_unlock_pin() -> String { } } +#[cfg(feature = "flutter")] +#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub fn get_trusted_devices() -> String { + if let Ok(Some(v)) = get_config("trusted-devices") { + v + } else { + Config::get_trusted_devices_json() + } +} + +#[cfg(feature = "flutter")] +#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub fn remove_trusted_devices(hwids: Vec) { + Config::remove_trusted_devices(&hwids); + allow_err!(set_data(&Data::RemoveTrustedDevices(hwids))); +} + +#[cfg(feature = "flutter")] +#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub fn clear_trusted_devices() { + Config::clear_trusted_devices(); + allow_err!(set_data(&Data::ClearTrustedDevices)); +} + pub fn get_id() -> String { if let Ok(Some(v)) = get_config("id") { // update salt also, so that next time reinstallation not causing first-time auto-login failure diff --git a/src/lang/ar.rs b/src/lang/ar.rs index d1da83ec576c..cf4790e433f1 100644 --- a/src/lang/ar.rs +++ b/src/lang/ar.rs @@ -636,5 +636,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Requires at least {} characters", ""), ("Wrong PIN", ""), ("Set PIN", ""), + ("Enable trusted devices", ""), + ("Manage trusted devices", ""), + ("Trust this device", "الوثوق بهذا الجهاز"), + ("Platform", ""), + ("Days remaining", ""), + ("enable-trusted-devices-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/be.rs b/src/lang/be.rs index a294fcae0cb3..81957bdc6550 100644 --- a/src/lang/be.rs +++ b/src/lang/be.rs @@ -636,5 +636,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Requires at least {} characters", ""), ("Wrong PIN", ""), ("Set PIN", ""), + ("Enable trusted devices", ""), + ("Manage trusted devices", ""), + ("Trust this device", "Даверыць гэтую прыладу"), + ("Platform", ""), + ("Days remaining", ""), + ("enable-trusted-devices-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/bg.rs b/src/lang/bg.rs index 9eb0ca63fd71..42d3752b673b 100644 --- a/src/lang/bg.rs +++ b/src/lang/bg.rs @@ -636,5 +636,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Requires at least {} characters", ""), ("Wrong PIN", ""), ("Set PIN", ""), + ("Enable trusted devices", ""), + ("Manage trusted devices", ""), + ("Trust this device", "Доверете се на това устройство"), + ("Platform", ""), + ("Days remaining", ""), + ("enable-trusted-devices-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ca.rs b/src/lang/ca.rs index 4721ed19cd7b..8ee0058beabe 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -636,5 +636,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Requires at least {} characters", ""), ("Wrong PIN", ""), ("Set PIN", ""), + ("Enable trusted devices", ""), + ("Manage trusted devices", ""), + ("Trust this device", "Confia en aquest dispositiu"), + ("Platform", ""), + ("Days remaining", ""), + ("enable-trusted-devices-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 6481761fdeb7..a50568ec448b 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -636,5 +636,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Requires at least {} characters", "不少于{}个字符"), ("Wrong PIN", "PIN 码错误"), ("Set PIN", "设置 PIN 码"), + ("Enable trusted devices", "启用信任设备"), + ("Manage trusted devices", "管理信任设备"), + ("Trust this device", "信任此设备"), + ("Platform", "平台"), + ("Days remaining", "剩余天数"), + ("enable-trusted-devices-tip", "允许受信任的设备跳过 2FA 验证"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index 2caf00e5d840..7c1c10a89583 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -636,5 +636,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Requires at least {} characters", ""), ("Wrong PIN", ""), ("Set PIN", ""), + ("Enable trusted devices", ""), + ("Manage trusted devices", ""), + ("Trust this device", "Důvěřovat tomuto zařízení"), + ("Platform", ""), + ("Days remaining", ""), + ("enable-trusted-devices-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index 44d19dd4600e..79f470461ce4 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -636,5 +636,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Requires at least {} characters", ""), ("Wrong PIN", ""), ("Set PIN", ""), + ("Enable trusted devices", ""), + ("Manage trusted devices", ""), + ("Trust this device", "Husk denne enhed"), + ("Platform", ""), + ("Days remaining", ""), + ("enable-trusted-devices-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index c31cc9d99e9c..c04550e255a4 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -636,5 +636,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Requires at least {} characters", "Erfordert mindestens {} Zeichen"), ("Wrong PIN", "Falsche PIN"), ("Set PIN", "PIN festlegen"), + ("Enable trusted devices", ""), + ("Manage trusted devices", ""), + ("Trust this device", "Diesem Gerät vertrauen"), + ("Platform", ""), + ("Days remaining", ""), + ("enable-trusted-devices-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/el.rs b/src/lang/el.rs index 2f929f3b1f8b..4af902b90008 100644 --- a/src/lang/el.rs +++ b/src/lang/el.rs @@ -636,5 +636,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Requires at least {} characters", ""), ("Wrong PIN", ""), ("Set PIN", ""), + ("Enable trusted devices", ""), + ("Manage trusted devices", ""), + ("Trust this device", "Εμπιστεύομαι αυτή την συσκευή"), + ("Platform", ""), + ("Days remaining", ""), + ("enable-trusted-devices-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/en.rs b/src/lang/en.rs index 638c1a608042..917422d0dc4f 100644 --- a/src/lang/en.rs +++ b/src/lang/en.rs @@ -232,6 +232,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("cancel-2fa-confirm-tip", "Are you sure you want to cancel 2FA?"), ("cancel-bot-confirm-tip", "Are you sure you want to cancel Telegram bot?"), ("About RustDesk", ""), - ("network_error_tip", "Please check your network connection, then click retry.") + ("network_error_tip", "Please check your network connection, then click retry."), + ("enable-trusted-devices-tip", "Skip 2FA verification on trusted devices"), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 70d73bfc608b..27ccf9945632 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -636,5 +636,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Requires at least {} characters", ""), ("Wrong PIN", ""), ("Set PIN", ""), + ("Enable trusted devices", ""), + ("Manage trusted devices", ""), + ("Trust this device", ""), + ("Platform", ""), + ("Days remaining", ""), + ("enable-trusted-devices-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index da6c2b4ca47c..b00bc5ce211b 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -636,5 +636,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Requires at least {} characters", ""), ("Wrong PIN", ""), ("Set PIN", ""), + ("Enable trusted devices", ""), + ("Manage trusted devices", ""), + ("Trust this device", "Confiar en este dispositivo"), + ("Platform", ""), + ("Days remaining", ""), + ("enable-trusted-devices-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/et.rs b/src/lang/et.rs index 74c30b076db1..967eba4213e0 100644 --- a/src/lang/et.rs +++ b/src/lang/et.rs @@ -636,5 +636,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Requires at least {} characters", ""), ("Wrong PIN", ""), ("Set PIN", ""), + ("Enable trusted devices", ""), + ("Manage trusted devices", ""), + ("Trust this device", ""), + ("Platform", ""), + ("Days remaining", ""), + ("enable-trusted-devices-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eu.rs b/src/lang/eu.rs index 55a9dcbfb254..307b3aff3f0b 100644 --- a/src/lang/eu.rs +++ b/src/lang/eu.rs @@ -636,5 +636,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Requires at least {} characters", ""), ("Wrong PIN", ""), ("Set PIN", ""), + ("Enable trusted devices", ""), + ("Manage trusted devices", ""), + ("Trust this device", "Gailu honetaz fidatu"), + ("Platform", ""), + ("Days remaining", ""), + ("enable-trusted-devices-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index 8166e0f924ca..b16e1439e79d 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -636,5 +636,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Requires at least {} characters", "حداقل به {} کاراکترها نیاز دارد"), ("Wrong PIN", "پین اشتباه است"), ("Set PIN", "پین را تنظیم کنید"), + ("Enable trusted devices", ""), + ("Manage trusted devices", ""), + ("Trust this device", "به این دستگاه اعتماد کنید"), + ("Platform", ""), + ("Days remaining", ""), + ("enable-trusted-devices-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index c64aa9ee2575..010b8a4b106d 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -636,5 +636,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Requires at least {} characters", ""), ("Wrong PIN", ""), ("Set PIN", ""), + ("Enable trusted devices", ""), + ("Manage trusted devices", ""), + ("Trust this device", "Faire confiance à cet appareil"), + ("Platform", ""), + ("Days remaining", ""), + ("enable-trusted-devices-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/he.rs b/src/lang/he.rs index 4e21c204d1a7..3dd423da0b20 100644 --- a/src/lang/he.rs +++ b/src/lang/he.rs @@ -636,5 +636,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Requires at least {} characters", ""), ("Wrong PIN", ""), ("Set PIN", ""), + ("Enable trusted devices", ""), + ("Manage trusted devices", ""), + ("Trust this device", ""), + ("Platform", ""), + ("Days remaining", ""), + ("enable-trusted-devices-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hr.rs b/src/lang/hr.rs index f221ebceee9f..392a6e825b37 100644 --- a/src/lang/hr.rs +++ b/src/lang/hr.rs @@ -636,5 +636,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Requires at least {} characters", ""), ("Wrong PIN", ""), ("Set PIN", ""), + ("Enable trusted devices", ""), + ("Manage trusted devices", ""), + ("Trust this device", "Vjeruj ovom uređaju"), + ("Platform", ""), + ("Days remaining", ""), + ("enable-trusted-devices-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 40fbf2f768a6..585a6a7114d2 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -636,5 +636,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Requires at least {} characters", ""), ("Wrong PIN", ""), ("Set PIN", ""), + ("Enable trusted devices", ""), + ("Manage trusted devices", ""), + ("Trust this device", ""), + ("Platform", ""), + ("Days remaining", ""), + ("enable-trusted-devices-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index eab2e1a36c38..8c1c6c9d0a0d 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -636,5 +636,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Requires at least {} characters", ""), ("Wrong PIN", ""), ("Set PIN", ""), + ("Enable trusted devices", ""), + ("Manage trusted devices", ""), + ("Trust this device", "Izinkan perangkat ini"), + ("Platform", ""), + ("Days remaining", ""), + ("enable-trusted-devices-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index d50b963b5119..3cecd680566a 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -636,5 +636,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Requires at least {} characters", "Richiede almeno {} caratteri"), ("Wrong PIN", "PIN errato"), ("Set PIN", "Imposta PIN"), + ("Enable trusted devices", ""), + ("Manage trusted devices", ""), + ("Trust this device", "Registra questo dispositivo come attendibile"), + ("Platform", ""), + ("Days remaining", ""), + ("enable-trusted-devices-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 8c8c71944198..471f796edb70 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -636,5 +636,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Requires at least {} characters", ""), ("Wrong PIN", ""), ("Set PIN", ""), + ("Enable trusted devices", ""), + ("Manage trusted devices", ""), + ("Trust this device", "このデバイスを信頼する"), + ("Platform", ""), + ("Days remaining", ""), + ("enable-trusted-devices-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 65db10aecb62..cd15163c2262 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -636,5 +636,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Requires at least {} characters", ""), ("Wrong PIN", ""), ("Set PIN", ""), + ("Enable trusted devices", ""), + ("Manage trusted devices", ""), + ("Trust this device", "이 장치 신뢰"), + ("Platform", ""), + ("Days remaining", ""), + ("enable-trusted-devices-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index 7545bff40631..74205198a551 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -636,5 +636,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Requires at least {} characters", ""), ("Wrong PIN", ""), ("Set PIN", ""), + ("Enable trusted devices", ""), + ("Manage trusted devices", ""), + ("Trust this device", ""), + ("Platform", ""), + ("Days remaining", ""), + ("enable-trusted-devices-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lt.rs b/src/lang/lt.rs index e659014e59dd..f5deb81e463b 100644 --- a/src/lang/lt.rs +++ b/src/lang/lt.rs @@ -636,5 +636,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Requires at least {} characters", ""), ("Wrong PIN", ""), ("Set PIN", ""), + ("Enable trusted devices", ""), + ("Manage trusted devices", ""), + ("Trust this device", "Pasitikėk šiuo įrenginiu"), + ("Platform", ""), + ("Days remaining", ""), + ("enable-trusted-devices-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lv.rs b/src/lang/lv.rs index 3296a51ae2ef..708724eb29e3 100644 --- a/src/lang/lv.rs +++ b/src/lang/lv.rs @@ -636,5 +636,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Requires at least {} characters", ""), ("Wrong PIN", ""), ("Set PIN", ""), + ("Enable trusted devices", ""), + ("Manage trusted devices", ""), + ("Trust this device", "Uzticēties šai ierīcei"), + ("Platform", ""), + ("Days remaining", ""), + ("enable-trusted-devices-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nb.rs b/src/lang/nb.rs index 1afa22d99b39..910d082bc377 100644 --- a/src/lang/nb.rs +++ b/src/lang/nb.rs @@ -636,5 +636,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Requires at least {} characters", ""), ("Wrong PIN", ""), ("Set PIN", ""), + ("Enable trusted devices", ""), + ("Manage trusted devices", ""), + ("Trust this device", "Husk denne enheten"), + ("Platform", ""), + ("Days remaining", ""), + ("enable-trusted-devices-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nl.rs b/src/lang/nl.rs index 187611393195..39435ac9c454 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -636,5 +636,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Requires at least {} characters", "Vereist minstens {} tekens"), ("Wrong PIN", "Verkeerde PIN-code"), ("Set PIN", "PIN-code instellen"), + ("Enable trusted devices", ""), + ("Manage trusted devices", ""), + ("Trust this device", "Vertrouw dit apparaat"), + ("Platform", ""), + ("Days remaining", ""), + ("enable-trusted-devices-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 5d04d76c58a4..de6f9334e902 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -636,5 +636,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Requires at least {} characters", ""), ("Wrong PIN", ""), ("Set PIN", ""), + ("Enable trusted devices", ""), + ("Manage trusted devices", ""), + ("Trust this device", "Dodaj to urządzenie do zaufanych"), + ("Platform", ""), + ("Days remaining", ""), + ("enable-trusted-devices-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index 965d934df9b9..d1b5d0adc3be 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -636,5 +636,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Requires at least {} characters", ""), ("Wrong PIN", ""), ("Set PIN", ""), + ("Enable trusted devices", ""), + ("Manage trusted devices", ""), + ("Trust this device", ""), + ("Platform", ""), + ("Days remaining", ""), + ("enable-trusted-devices-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index 7bf00d20734d..8745269ceebd 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -636,5 +636,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Requires at least {} characters", ""), ("Wrong PIN", "PIN Errado"), ("Set PIN", "Definir PIN"), + ("Enable trusted devices", ""), + ("Manage trusted devices", ""), + ("Trust this device", "Confiar neste dispositivo"), + ("Platform", ""), + ("Days remaining", ""), + ("enable-trusted-devices-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ro.rs b/src/lang/ro.rs index 639f0a29c9ff..d0ebb7b56c86 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -636,5 +636,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Requires at least {} characters", ""), ("Wrong PIN", ""), ("Set PIN", ""), + ("Enable trusted devices", ""), + ("Manage trusted devices", ""), + ("Trust this device", "Acest dispozitiv este de încredere"), + ("Platform", ""), + ("Days remaining", ""), + ("enable-trusted-devices-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index f3fe46f3a292..94f7138507d9 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -636,5 +636,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Requires at least {} characters", "Требуется не менее {} символов"), ("Wrong PIN", "Неправильный PIN-код"), ("Set PIN", "Установить PIN-код"), + ("Enable trusted devices", ""), + ("Manage trusted devices", ""), + ("Trust this device", "Доверенное устройство"), + ("Platform", ""), + ("Days remaining", ""), + ("enable-trusted-devices-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 816106c57710..466631ac17e9 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -636,5 +636,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Requires at least {} characters", ""), ("Wrong PIN", ""), ("Set PIN", ""), + ("Enable trusted devices", ""), + ("Manage trusted devices", ""), + ("Trust this device", "Dôverovať tomuto zariadeniu"), + ("Platform", ""), + ("Days remaining", ""), + ("enable-trusted-devices-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sl.rs b/src/lang/sl.rs index 1dd8280c6c67..92301f3fa7d3 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -636,5 +636,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Requires at least {} characters", ""), ("Wrong PIN", ""), ("Set PIN", ""), + ("Enable trusted devices", ""), + ("Manage trusted devices", ""), + ("Trust this device", ""), + ("Platform", ""), + ("Days remaining", ""), + ("enable-trusted-devices-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sq.rs b/src/lang/sq.rs index f11d485605e7..f2cf322a9179 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -636,5 +636,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Requires at least {} characters", ""), ("Wrong PIN", ""), ("Set PIN", ""), + ("Enable trusted devices", ""), + ("Manage trusted devices", ""), + ("Trust this device", ""), + ("Platform", ""), + ("Days remaining", ""), + ("enable-trusted-devices-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sr.rs b/src/lang/sr.rs index 2bc72e6e368b..f5725d1e7aa5 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -636,5 +636,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Requires at least {} characters", ""), ("Wrong PIN", ""), ("Set PIN", ""), + ("Enable trusted devices", ""), + ("Manage trusted devices", ""), + ("Trust this device", ""), + ("Platform", ""), + ("Days remaining", ""), + ("enable-trusted-devices-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sv.rs b/src/lang/sv.rs index ed7752e262e5..52d547fe3568 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -636,5 +636,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Requires at least {} characters", ""), ("Wrong PIN", ""), ("Set PIN", ""), + ("Enable trusted devices", ""), + ("Manage trusted devices", ""), + ("Trust this device", "Lita på denna enhet"), + ("Platform", ""), + ("Days remaining", ""), + ("enable-trusted-devices-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index 7e57544a8c6f..fa89e2bdf36c 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -636,5 +636,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Requires at least {} characters", ""), ("Wrong PIN", ""), ("Set PIN", ""), + ("Enable trusted devices", ""), + ("Manage trusted devices", ""), + ("Trust this device", ""), + ("Platform", ""), + ("Days remaining", ""), + ("enable-trusted-devices-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/th.rs b/src/lang/th.rs index 6789a45731b4..5fe176a8c1f2 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -636,5 +636,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Requires at least {} characters", ""), ("Wrong PIN", ""), ("Set PIN", ""), + ("Enable trusted devices", ""), + ("Manage trusted devices", ""), + ("Trust this device", "เชื่อถืออุปกรณ์นี้"), + ("Platform", ""), + ("Days remaining", ""), + ("enable-trusted-devices-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index e9b3d6cf2eb6..d0dc484c7eb7 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -636,5 +636,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Requires at least {} characters", ""), ("Wrong PIN", ""), ("Set PIN", ""), + ("Enable trusted devices", ""), + ("Manage trusted devices", ""), + ("Trust this device", ""), + ("Platform", ""), + ("Days remaining", ""), + ("enable-trusted-devices-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 0a4b26144457..a9ffa45d6f65 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -636,5 +636,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Requires at least {} characters", ""), ("Wrong PIN", ""), ("Set PIN", ""), + ("Enable trusted devices", ""), + ("Manage trusted devices", ""), + ("Trust this device", "信任此裝置"), + ("Platform", ""), + ("Days remaining", ""), + ("enable-trusted-devices-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/uk.rs b/src/lang/uk.rs index 3d031a65e791..a7f37014d083 100644 --- a/src/lang/uk.rs +++ b/src/lang/uk.rs @@ -636,5 +636,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Requires at least {} characters", "Потрібно щонайменше {} символів"), ("Wrong PIN", "Неправильний PIN-код"), ("Set PIN", "Встановити PIN-код"), + ("Enable trusted devices", ""), + ("Manage trusted devices", ""), + ("Trust this device", "Довірений пристрій"), + ("Platform", ""), + ("Days remaining", ""), + ("enable-trusted-devices-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index 0953643624f1..dadc0a154369 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -636,5 +636,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Requires at least {} characters", ""), ("Wrong PIN", ""), ("Set PIN", ""), + ("Enable trusted devices", ""), + ("Manage trusted devices", ""), + ("Trust this device", "Tin thiết bị này"), + ("Platform", ""), + ("Days remaining", ""), + ("enable-trusted-devices-tip", ""), ].iter().cloned().collect(); } diff --git a/src/server/connection.rs b/src/server/connection.rs index bc8c4ef0e34e..929d040e2413 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -27,7 +27,7 @@ use hbb_common::platform::linux::run_cmds; #[cfg(target_os = "android")] use hbb_common::protobuf::EnumOrUnknown; use hbb_common::{ - config::{self, Config}, + config::{self, Config, TrustedDevice}, fs::{self, can_enable_overwrite_detection}, futures::{SinkExt, StreamExt}, get_time, get_version_number, @@ -1482,6 +1482,9 @@ impl Connection { let mut msg_out = Message::new(); let mut res = LoginResponse::new(); res.set_error(err.to_string()); + if err.to_string() == crate::client::REQUIRE_2FA { + res.enable_trusted_devices = Self::enable_trusted_devices(); + } msg_out.set_login_response(res); self.send(msg_out).await; } @@ -1623,11 +1626,32 @@ impl Connection { } } + #[inline] + fn enable_trusted_devices() -> bool { + config::option2bool( + config::keys::OPTION_ENABLE_TRUSTED_DEVICES, + &Config::get_option(config::keys::OPTION_ENABLE_TRUSTED_DEVICES), + ) + } + async fn handle_login_request_without_validation(&mut self, lr: &LoginRequest) { self.lr = lr.clone(); if let Some(o) = lr.option.as_ref() { self.options_in_login = Some(o.clone()); } + if self.require_2fa.is_some() && !lr.hwid.is_empty() && Self::enable_trusted_devices() { + let devices = Config::get_trusted_devices(); + if let Some(device) = devices.iter().find(|d| d.hwid == lr.hwid) { + if !device.outdate() + && device.id == lr.my_id + && device.name == lr.my_name + && device.platform == lr.my_platform + { + log::info!("2FA bypassed by trusted devices"); + self.require_2fa = None; + } + } + } self.video_ack_required = lr.video_ack_required; } @@ -1841,6 +1865,15 @@ impl Connection { }, ); } + if !tfa.hwid.is_empty() && Self::enable_trusted_devices() { + Config::add_trusted_device(TrustedDevice { + hwid: tfa.hwid, + time: hbb_common::get_time(), + id: self.lr.my_id.clone(), + name: self.lr.my_name.clone(), + platform: self.lr.my_platform.clone(), + }); + } } else { self.update_failure(failure, false, 1); self.send_login_error(crate::client::LOGIN_MSG_2FA_WRONG) diff --git a/src/ui/common.tis b/src/ui/common.tis index 0f154a74cba5..b6d2b8ee2874 100644 --- a/src/ui/common.tis +++ b/src/ui/common.tis @@ -268,7 +268,7 @@ function msgbox(type, title, content, link="", callback=null, height=180, width= view.close(); return; } - handler.send2fa(res.code); + handler.send2fa(res.code, res.trust_this_device || false); msgbox("connecting", "Connecting...", "Logging in..."); }; } else if (type == "session-login" || type == "session-re-login") { diff --git a/src/ui/msgbox.tis b/src/ui/msgbox.tis index a8fa79ad4a5c..34f6b04432d7 100644 --- a/src/ui/msgbox.tis +++ b/src/ui/msgbox.tis @@ -66,9 +66,11 @@ class MsgboxComponent: Reactor.Component { } function get2faContent() { + var enable_trusted_devices = handler.get_enable_trusted_devices(); return
{translate('enter-2fa-title')}
+ {enable_trusted_devices ?
{translate('Trust this device')}
: ""}
; } diff --git a/src/ui/remote.rs b/src/ui/remote.rs index 93a796f4bd5c..0ad84e8ed15a 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -433,7 +433,8 @@ impl sciter::EventHandler for SciterSession { fn is_port_forward(); fn is_rdp(); fn login(String, String, String, bool); - fn send2fa(String); + fn send2fa(String, bool); + fn get_enable_trusted_devices(); fn new_rdp(); fn send_mouse(i32, i32, i32, bool, bool, bool, bool); fn enter(String); diff --git a/src/ui_interface.rs b/src/ui_interface.rs index c39a58068880..9c3864f0c3c9 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -1471,3 +1471,28 @@ pub fn set_unlock_pin(pin: String) -> String { Err(err) => err.to_string(), } } + +#[cfg(feature = "flutter")] +pub fn get_trusted_devices() -> String { + #[cfg(any(target_os = "android", target_os = "ios"))] + return Config::get_trusted_devices_json(); + #[cfg(not(any(target_os = "android", target_os = "ios")))] + return ipc::get_trusted_devices(); +} + +#[cfg(feature = "flutter")] +pub fn remove_trusted_devices(json: &str) { + let hwids = serde_json::from_str::>(json).unwrap_or_default(); + #[cfg(any(target_os = "android", target_os = "ios"))] + Config::remove_trusted_devices(&hwids); + #[cfg(not(any(target_os = "android", target_os = "ios")))] + ipc::remove_trusted_devices(hwids); +} + +#[cfg(feature = "flutter")] +pub fn clear_trusted_devices() { + #[cfg(any(target_os = "android", target_os = "ios"))] + Config::clear_trusted_devices(); + #[cfg(not(any(target_os = "android", target_os = "ios")))] + ipc::clear_trusted_devices(); +} diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 02fdf1caa1ab..0e030aa8ba47 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -1156,15 +1156,29 @@ impl Session { self.send(Data::Login((os_username, os_password, password, remember))); } - pub fn send2fa(&self, code: String) { + pub fn send2fa(&self, code: String, trust_this_device: bool) { let mut msg_out = Message::new(); + let hwid = if trust_this_device { + crate::get_hwid() + } else { + Bytes::new() + }; + self.lc.write().unwrap().set_option( + "trust-this-device".to_string(), + if trust_this_device { "Y" } else { "" }.to_string(), + ); msg_out.set_auth_2fa(Auth2FA { code, + hwid, ..Default::default() }); self.send(Data::Message(msg_out)); } + pub fn get_enable_trusted_devices(&self) -> bool { + self.lc.read().unwrap().enable_trusted_devices + } + pub fn new_rdp(&self) { self.send(Data::NewRDP); } From 83bf067d187624ceb04c4765bf3148cb19f0767a Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Mon, 12 Aug 2024 21:53:41 +0800 Subject: [PATCH 087/541] fix: not plug virtual dislay, non win, installed (#9034) Signed-off-by: fufesou --- src/client/io_loop.rs | 45 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index 537de56add1b..15c6c24b537f 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -73,6 +73,22 @@ pub struct Remote { fps_control: FpsControl, decode_fps: Arc>>, chroma: Arc>>, + peer_info: ParsedPeerInfo, +} + +#[derive(Default)] +struct ParsedPeerInfo { + platform: String, + is_installed: bool, + idd_impl: String, +} + +impl ParsedPeerInfo { + fn is_support_virtual_display(&self) -> bool { + self.is_installed + && self.platform == "Windows" + && (self.idd_impl == "rustdesk_idd" || self.idd_impl == "amyuni_idd") + } } impl Remote { @@ -112,6 +128,7 @@ impl Remote { fps_control: Default::default(), decode_fps, chroma, + peer_info: Default::default(), } } @@ -938,6 +955,9 @@ impl Remote { } async fn send_toggle_virtual_display_msg(&self, peer: &mut Stream) { + if !self.peer_info.is_support_virtual_display() { + return; + } let lc = self.handler.lc.read().unwrap(); let displays = lc.get_option("virtual-display"); for d in displays.split(',') { @@ -961,6 +981,11 @@ impl Remote { && lc.get_toggle_option("privacy-mode") { let impl_key = lc.get_option("privacy-mode-impl-key"); + if impl_key == crate::privacy_mode::PRIVACY_MODE_IMPL_WIN_VIRTUAL_DISPLAY + && !self.peer_info.is_support_virtual_display() + { + return; + } let mut misc = Misc::new(); misc.set_toggle_privacy_mode(TogglePrivacyMode { impl_key, @@ -1146,6 +1171,7 @@ impl Remote { Some(login_response::Union::PeerInfo(pi)) => { let peer_version = pi.version.clone(); let peer_platform = pi.platform.clone(); + self.set_peer_info(&pi); self.handler.handle_peer_info(pi); self.check_clipboard_file_context(); if !(self.handler.is_file_transfer() || self.handler.is_port_forward()) { @@ -1636,6 +1662,25 @@ impl Remote { true } + fn set_peer_info(&mut self, pi: &PeerInfo) { + self.peer_info.platform = pi.platform.clone(); + if let Ok(platform_additions) = + serde_json::from_str::>(&pi.platform_additions) + { + self.peer_info.is_installed = platform_additions + .get("is_installed") + .map(|v| v.as_bool()) + .flatten() + .unwrap_or(false); + self.peer_info.idd_impl = platform_additions + .get("idd_impl") + .map(|v| v.as_str()) + .flatten() + .unwrap_or_default() + .to_string(); + } + } + async fn handle_back_notification(&mut self, notification: BackNotification) -> bool { match notification.union { Some(back_notification::Union::BlockInputState(state)) => { From 8f00067266140f824b7bdabbdd3ec4d3c2b92b7a Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Mon, 12 Aug 2024 22:15:59 +0800 Subject: [PATCH 088/541] fix: build (#9036) Signed-off-by: fufesou --- src/privacy_mode.rs | 11 +++-------- src/privacy_mode/win_exclude_from_capture.rs | 2 +- src/privacy_mode/win_mag.rs | 2 +- src/privacy_mode/win_virtual_display.rs | 2 +- 4 files changed, 6 insertions(+), 11 deletions(-) diff --git a/src/privacy_mode.rs b/src/privacy_mode.rs index 8b1510625ca5..a02b8bc93eba 100644 --- a/src/privacy_mode.rs +++ b/src/privacy_mode.rs @@ -38,14 +38,9 @@ pub const TURN_OFF_OTHER_ID: &'static str = "Failed to turn off privacy mode that belongs to someone else."; pub const NO_PHYSICAL_DISPLAYS: &'static str = "no_need_privacy_mode_no_physical_displays_tip"; -#[cfg(windows)] -pub const PRIVACY_MODE_IMPL_WIN_MAG: &str = win_mag::PRIVACY_MODE_IMPL; -#[cfg(windows)] -pub const PRIVACY_MODE_IMPL_WIN_EXCLUDE_FROM_CAPTURE: &str = - win_exclude_from_capture::PRIVACY_MODE_IMPL; - -#[cfg(windows)] -pub const PRIVACY_MODE_IMPL_WIN_VIRTUAL_DISPLAY: &str = win_virtual_display::PRIVACY_MODE_IMPL; +pub const PRIVACY_MODE_IMPL_WIN_MAG: &str = "privacy_mode_impl_mag"; +pub const PRIVACY_MODE_IMPL_WIN_EXCLUDE_FROM_CAPTURE: &str = "privacy_mode_impl_exclude_from_capture"; +pub const PRIVACY_MODE_IMPL_WIN_VIRTUAL_DISPLAY: &str = "privacy_mode_impl_virtual_display"; #[derive(Debug, Serialize, Deserialize, Clone)] #[serde(tag = "t", content = "c")] diff --git a/src/privacy_mode/win_exclude_from_capture.rs b/src/privacy_mode/win_exclude_from_capture.rs index 63164f838fb1..7d680011f0af 100644 --- a/src/privacy_mode/win_exclude_from_capture.rs +++ b/src/privacy_mode/win_exclude_from_capture.rs @@ -2,7 +2,7 @@ use hbb_common::platform::windows::is_windows_version_or_greater; pub use super::win_topmost_window::PrivacyModeImpl; -pub(super) const PRIVACY_MODE_IMPL: &str = "privacy_mode_impl_exclude_from_capture"; +pub(super) const PRIVACY_MODE_IMPL: &str = super::PRIVACY_MODE_IMPL_WIN_EXCLUDE_FROM_CAPTURE; pub(super) fn is_supported() -> bool { // https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setwindowdisplayaffinity diff --git a/src/privacy_mode/win_mag.rs b/src/privacy_mode/win_mag.rs index a93dce350286..6235c24838e2 100644 --- a/src/privacy_mode/win_mag.rs +++ b/src/privacy_mode/win_mag.rs @@ -4,7 +4,7 @@ use std::time::Instant; pub use super::win_topmost_window::PrivacyModeImpl; -pub(super) const PRIVACY_MODE_IMPL: &str = "privacy_mode_impl_mag"; +pub(super) const PRIVACY_MODE_IMPL: &str = super::PRIVACY_MODE_IMPL_WIN_MAG; pub fn create_capturer( privacy_mode_id: i32, diff --git a/src/privacy_mode/win_virtual_display.rs b/src/privacy_mode/win_virtual_display.rs index 7e322ebbf7e2..afaafa86529a 100644 --- a/src/privacy_mode/win_virtual_display.rs +++ b/src/privacy_mode/win_virtual_display.rs @@ -27,7 +27,7 @@ use winapi::{ }, }; -pub(super) const PRIVACY_MODE_IMPL: &str = "privacy_mode_impl_virtual_display"; +pub(super) const PRIVACY_MODE_IMPL: &str = super::PRIVACY_MODE_IMPL_WIN_VIRTUAL_DISPLAY; const CONFIG_KEY_REG_RECOVERY: &str = "reg_recovery"; From b8b3a089f36f7e68e9ec9c48f11f27fa5eeda7fb Mon Sep 17 00:00:00 2001 From: 21pages Date: Mon, 12 Aug 2024 22:20:35 +0800 Subject: [PATCH 089/541] android telebot setting (#9035) Signed-off-by: 21pages --- flutter/lib/mobile/pages/settings_page.dart | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/flutter/lib/mobile/pages/settings_page.dart b/flutter/lib/mobile/pages/settings_page.dart index 98a2b9f50c1b..ca1ca1afa478 100644 --- a/flutter/lib/mobile/pages/settings_page.dart +++ b/flutter/lib/mobile/pages/settings_page.dart @@ -266,6 +266,25 @@ class _SettingsState extends State with WidgetsBindingObserver { } }, ), + if (enable2fa) + SettingsTile.switchTile( + title: Text(translate('Telegram bot')), + initialValue: bind.mainHasValidBotSync(), + onToggle: (v) async { + update() async { + setState(() {}); + } + + if (v == false) { + CommonConfirmDialog( + gFFI.dialogManager, translate('cancel-bot-confirm-tip'), () { + changeBot(callback: update); + }); + } else { + changeBot(callback: update); + } + }, + ), if (enable2fa) SettingsTile.switchTile( title: Column( From a771abcdc2c71762448d38ae493bae0e72101cc8 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Tue, 13 Aug 2024 00:18:35 +0800 Subject: [PATCH 090/541] fix: win, multi monitors, maximize, may fix 9033 (#9038) Signed-off-by: fufesou --- flutter/pubspec.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index 7b3b77d6d1fe..eb8426572ca4 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -335,7 +335,7 @@ packages: description: path: "." ref: HEAD - resolved-ref: "0d4606f95b3926566aeacab16c6db1cb9ce3d3fa" + resolved-ref: cd323a66d0771aeea4bc243a203eacfb6f89e3e4 url: "https://github.com/rustdesk-org/rustdesk_desktop_multi_window" source: git version: "0.1.0" From a3c5adb1f435d0e0516c550fcc201fd8822c1c89 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Tue, 13 Aug 2024 11:27:39 +0800 Subject: [PATCH 091/541] refact: remove dup translation (#9043) Signed-off-by: fufesou --- src/lang/ar.rs | 1 - src/lang/be.rs | 1 - src/lang/bg.rs | 1 - src/lang/ca.rs | 1 - src/lang/cn.rs | 1 - src/lang/cs.rs | 1 - src/lang/da.rs | 1 - src/lang/de.rs | 1 - src/lang/el.rs | 1 - src/lang/eo.rs | 1 - src/lang/es.rs | 1 - src/lang/et.rs | 1 - src/lang/eu.rs | 1 - src/lang/fa.rs | 1 - src/lang/fr.rs | 1 - src/lang/he.rs | 1 - src/lang/hr.rs | 1 - src/lang/hu.rs | 1 - src/lang/id.rs | 1 - src/lang/it.rs | 1 - src/lang/ja.rs | 1 - src/lang/ko.rs | 1 - src/lang/kz.rs | 1 - src/lang/lt.rs | 1 - src/lang/lv.rs | 1 - src/lang/nb.rs | 1 - src/lang/nl.rs | 1 - src/lang/pl.rs | 1 - src/lang/pt_PT.rs | 1 - src/lang/ptbr.rs | 1 - src/lang/ro.rs | 1 - src/lang/ru.rs | 1 - src/lang/sk.rs | 1 - src/lang/sl.rs | 1 - src/lang/sq.rs | 1 - src/lang/sr.rs | 1 - src/lang/sv.rs | 1 - src/lang/template.rs | 1 - src/lang/th.rs | 1 - src/lang/tr.rs | 1 - src/lang/tw.rs | 1 - src/lang/uk.rs | 1 - src/lang/vn.rs | 1 - 43 files changed, 43 deletions(-) diff --git a/src/lang/ar.rs b/src/lang/ar.rs index cf4790e433f1..d1f6d35e3274 100644 --- a/src/lang/ar.rs +++ b/src/lang/ar.rs @@ -638,7 +638,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Set PIN", ""), ("Enable trusted devices", ""), ("Manage trusted devices", ""), - ("Trust this device", "الوثوق بهذا الجهاز"), ("Platform", ""), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), diff --git a/src/lang/be.rs b/src/lang/be.rs index 81957bdc6550..6bf449df220c 100644 --- a/src/lang/be.rs +++ b/src/lang/be.rs @@ -638,7 +638,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Set PIN", ""), ("Enable trusted devices", ""), ("Manage trusted devices", ""), - ("Trust this device", "Даверыць гэтую прыладу"), ("Platform", ""), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), diff --git a/src/lang/bg.rs b/src/lang/bg.rs index 42d3752b673b..dfdfaa85d61f 100644 --- a/src/lang/bg.rs +++ b/src/lang/bg.rs @@ -638,7 +638,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Set PIN", ""), ("Enable trusted devices", ""), ("Manage trusted devices", ""), - ("Trust this device", "Доверете се на това устройство"), ("Platform", ""), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), diff --git a/src/lang/ca.rs b/src/lang/ca.rs index 8ee0058beabe..4a98333fd337 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -638,7 +638,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Set PIN", ""), ("Enable trusted devices", ""), ("Manage trusted devices", ""), - ("Trust this device", "Confia en aquest dispositiu"), ("Platform", ""), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), diff --git a/src/lang/cn.rs b/src/lang/cn.rs index a50568ec448b..0768bd25e599 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -638,7 +638,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Set PIN", "设置 PIN 码"), ("Enable trusted devices", "启用信任设备"), ("Manage trusted devices", "管理信任设备"), - ("Trust this device", "信任此设备"), ("Platform", "平台"), ("Days remaining", "剩余天数"), ("enable-trusted-devices-tip", "允许受信任的设备跳过 2FA 验证"), diff --git a/src/lang/cs.rs b/src/lang/cs.rs index 7c1c10a89583..2531a263c67c 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -638,7 +638,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Set PIN", ""), ("Enable trusted devices", ""), ("Manage trusted devices", ""), - ("Trust this device", "Důvěřovat tomuto zařízení"), ("Platform", ""), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), diff --git a/src/lang/da.rs b/src/lang/da.rs index 79f470461ce4..11d6516b57cd 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -638,7 +638,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Set PIN", ""), ("Enable trusted devices", ""), ("Manage trusted devices", ""), - ("Trust this device", "Husk denne enhed"), ("Platform", ""), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), diff --git a/src/lang/de.rs b/src/lang/de.rs index c04550e255a4..3e36b246a43a 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -638,7 +638,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Set PIN", "PIN festlegen"), ("Enable trusted devices", ""), ("Manage trusted devices", ""), - ("Trust this device", "Diesem Gerät vertrauen"), ("Platform", ""), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), diff --git a/src/lang/el.rs b/src/lang/el.rs index 4af902b90008..e5331c104277 100644 --- a/src/lang/el.rs +++ b/src/lang/el.rs @@ -638,7 +638,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Set PIN", ""), ("Enable trusted devices", ""), ("Manage trusted devices", ""), - ("Trust this device", "Εμπιστεύομαι αυτή την συσκευή"), ("Platform", ""), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 27ccf9945632..ffa4d3b7de4a 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -638,7 +638,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Set PIN", ""), ("Enable trusted devices", ""), ("Manage trusted devices", ""), - ("Trust this device", ""), ("Platform", ""), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), diff --git a/src/lang/es.rs b/src/lang/es.rs index b00bc5ce211b..4ed1a2d62d2b 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -638,7 +638,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Set PIN", ""), ("Enable trusted devices", ""), ("Manage trusted devices", ""), - ("Trust this device", "Confiar en este dispositivo"), ("Platform", ""), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), diff --git a/src/lang/et.rs b/src/lang/et.rs index 967eba4213e0..70c834a6cd2a 100644 --- a/src/lang/et.rs +++ b/src/lang/et.rs @@ -638,7 +638,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Set PIN", ""), ("Enable trusted devices", ""), ("Manage trusted devices", ""), - ("Trust this device", ""), ("Platform", ""), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), diff --git a/src/lang/eu.rs b/src/lang/eu.rs index 307b3aff3f0b..3bb756859330 100644 --- a/src/lang/eu.rs +++ b/src/lang/eu.rs @@ -638,7 +638,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Set PIN", ""), ("Enable trusted devices", ""), ("Manage trusted devices", ""), - ("Trust this device", "Gailu honetaz fidatu"), ("Platform", ""), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), diff --git a/src/lang/fa.rs b/src/lang/fa.rs index b16e1439e79d..898596a15e95 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -638,7 +638,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Set PIN", "پین را تنظیم کنید"), ("Enable trusted devices", ""), ("Manage trusted devices", ""), - ("Trust this device", "به این دستگاه اعتماد کنید"), ("Platform", ""), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 010b8a4b106d..c0eefab3639c 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -638,7 +638,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Set PIN", ""), ("Enable trusted devices", ""), ("Manage trusted devices", ""), - ("Trust this device", "Faire confiance à cet appareil"), ("Platform", ""), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), diff --git a/src/lang/he.rs b/src/lang/he.rs index 3dd423da0b20..45fefe666894 100644 --- a/src/lang/he.rs +++ b/src/lang/he.rs @@ -638,7 +638,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Set PIN", ""), ("Enable trusted devices", ""), ("Manage trusted devices", ""), - ("Trust this device", ""), ("Platform", ""), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), diff --git a/src/lang/hr.rs b/src/lang/hr.rs index 392a6e825b37..599b87c7b7b8 100644 --- a/src/lang/hr.rs +++ b/src/lang/hr.rs @@ -638,7 +638,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Set PIN", ""), ("Enable trusted devices", ""), ("Manage trusted devices", ""), - ("Trust this device", "Vjeruj ovom uređaju"), ("Platform", ""), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 585a6a7114d2..ef8d0d1931c5 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -638,7 +638,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Set PIN", ""), ("Enable trusted devices", ""), ("Manage trusted devices", ""), - ("Trust this device", ""), ("Platform", ""), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), diff --git a/src/lang/id.rs b/src/lang/id.rs index 8c1c6c9d0a0d..f814a5c21c2b 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -638,7 +638,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Set PIN", ""), ("Enable trusted devices", ""), ("Manage trusted devices", ""), - ("Trust this device", "Izinkan perangkat ini"), ("Platform", ""), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), diff --git a/src/lang/it.rs b/src/lang/it.rs index 3cecd680566a..ae814fe360a8 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -638,7 +638,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Set PIN", "Imposta PIN"), ("Enable trusted devices", ""), ("Manage trusted devices", ""), - ("Trust this device", "Registra questo dispositivo come attendibile"), ("Platform", ""), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 471f796edb70..27736008ca70 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -638,7 +638,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Set PIN", ""), ("Enable trusted devices", ""), ("Manage trusted devices", ""), - ("Trust this device", "このデバイスを信頼する"), ("Platform", ""), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), diff --git a/src/lang/ko.rs b/src/lang/ko.rs index cd15163c2262..841f5f7062b2 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -638,7 +638,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Set PIN", ""), ("Enable trusted devices", ""), ("Manage trusted devices", ""), - ("Trust this device", "이 장치 신뢰"), ("Platform", ""), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), diff --git a/src/lang/kz.rs b/src/lang/kz.rs index 74205198a551..7fe5b8b38d64 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -638,7 +638,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Set PIN", ""), ("Enable trusted devices", ""), ("Manage trusted devices", ""), - ("Trust this device", ""), ("Platform", ""), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), diff --git a/src/lang/lt.rs b/src/lang/lt.rs index f5deb81e463b..922bb1881a0c 100644 --- a/src/lang/lt.rs +++ b/src/lang/lt.rs @@ -638,7 +638,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Set PIN", ""), ("Enable trusted devices", ""), ("Manage trusted devices", ""), - ("Trust this device", "Pasitikėk šiuo įrenginiu"), ("Platform", ""), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), diff --git a/src/lang/lv.rs b/src/lang/lv.rs index 708724eb29e3..8b00c360ee37 100644 --- a/src/lang/lv.rs +++ b/src/lang/lv.rs @@ -638,7 +638,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Set PIN", ""), ("Enable trusted devices", ""), ("Manage trusted devices", ""), - ("Trust this device", "Uzticēties šai ierīcei"), ("Platform", ""), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), diff --git a/src/lang/nb.rs b/src/lang/nb.rs index 910d082bc377..0ce11a845e8f 100644 --- a/src/lang/nb.rs +++ b/src/lang/nb.rs @@ -638,7 +638,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Set PIN", ""), ("Enable trusted devices", ""), ("Manage trusted devices", ""), - ("Trust this device", "Husk denne enheten"), ("Platform", ""), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), diff --git a/src/lang/nl.rs b/src/lang/nl.rs index 39435ac9c454..2873fb474dfd 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -638,7 +638,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Set PIN", "PIN-code instellen"), ("Enable trusted devices", ""), ("Manage trusted devices", ""), - ("Trust this device", "Vertrouw dit apparaat"), ("Platform", ""), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), diff --git a/src/lang/pl.rs b/src/lang/pl.rs index de6f9334e902..5b8061c5936e 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -638,7 +638,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Set PIN", ""), ("Enable trusted devices", ""), ("Manage trusted devices", ""), - ("Trust this device", "Dodaj to urządzenie do zaufanych"), ("Platform", ""), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index d1b5d0adc3be..1ec66daee4bc 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -638,7 +638,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Set PIN", ""), ("Enable trusted devices", ""), ("Manage trusted devices", ""), - ("Trust this device", ""), ("Platform", ""), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index 8745269ceebd..8983413f5691 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -638,7 +638,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Set PIN", "Definir PIN"), ("Enable trusted devices", ""), ("Manage trusted devices", ""), - ("Trust this device", "Confiar neste dispositivo"), ("Platform", ""), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), diff --git a/src/lang/ro.rs b/src/lang/ro.rs index d0ebb7b56c86..b913fdac3507 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -638,7 +638,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Set PIN", ""), ("Enable trusted devices", ""), ("Manage trusted devices", ""), - ("Trust this device", "Acest dispozitiv este de încredere"), ("Platform", ""), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 94f7138507d9..965bc0ce6f9e 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -638,7 +638,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Set PIN", "Установить PIN-код"), ("Enable trusted devices", ""), ("Manage trusted devices", ""), - ("Trust this device", "Доверенное устройство"), ("Platform", ""), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 466631ac17e9..5c3247374a3c 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -638,7 +638,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Set PIN", ""), ("Enable trusted devices", ""), ("Manage trusted devices", ""), - ("Trust this device", "Dôverovať tomuto zariadeniu"), ("Platform", ""), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), diff --git a/src/lang/sl.rs b/src/lang/sl.rs index 92301f3fa7d3..0973fdac807d 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -638,7 +638,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Set PIN", ""), ("Enable trusted devices", ""), ("Manage trusted devices", ""), - ("Trust this device", ""), ("Platform", ""), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), diff --git a/src/lang/sq.rs b/src/lang/sq.rs index f2cf322a9179..bf89fd05f11e 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -638,7 +638,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Set PIN", ""), ("Enable trusted devices", ""), ("Manage trusted devices", ""), - ("Trust this device", ""), ("Platform", ""), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), diff --git a/src/lang/sr.rs b/src/lang/sr.rs index f5725d1e7aa5..987b88e4f432 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -638,7 +638,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Set PIN", ""), ("Enable trusted devices", ""), ("Manage trusted devices", ""), - ("Trust this device", ""), ("Platform", ""), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), diff --git a/src/lang/sv.rs b/src/lang/sv.rs index 52d547fe3568..d079cdfbdb6d 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -638,7 +638,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Set PIN", ""), ("Enable trusted devices", ""), ("Manage trusted devices", ""), - ("Trust this device", "Lita på denna enhet"), ("Platform", ""), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), diff --git a/src/lang/template.rs b/src/lang/template.rs index fa89e2bdf36c..e11bb0663d31 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -638,7 +638,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Set PIN", ""), ("Enable trusted devices", ""), ("Manage trusted devices", ""), - ("Trust this device", ""), ("Platform", ""), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), diff --git a/src/lang/th.rs b/src/lang/th.rs index 5fe176a8c1f2..0ab7963481a5 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -638,7 +638,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Set PIN", ""), ("Enable trusted devices", ""), ("Manage trusted devices", ""), - ("Trust this device", "เชื่อถืออุปกรณ์นี้"), ("Platform", ""), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), diff --git a/src/lang/tr.rs b/src/lang/tr.rs index d0dc484c7eb7..98f07122ffcc 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -638,7 +638,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Set PIN", ""), ("Enable trusted devices", ""), ("Manage trusted devices", ""), - ("Trust this device", ""), ("Platform", ""), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), diff --git a/src/lang/tw.rs b/src/lang/tw.rs index a9ffa45d6f65..d935b2dae20c 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -638,7 +638,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Set PIN", ""), ("Enable trusted devices", ""), ("Manage trusted devices", ""), - ("Trust this device", "信任此裝置"), ("Platform", ""), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), diff --git a/src/lang/uk.rs b/src/lang/uk.rs index a7f37014d083..6465299f7494 100644 --- a/src/lang/uk.rs +++ b/src/lang/uk.rs @@ -638,7 +638,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Set PIN", "Встановити PIN-код"), ("Enable trusted devices", ""), ("Manage trusted devices", ""), - ("Trust this device", "Довірений пристрій"), ("Platform", ""), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), diff --git a/src/lang/vn.rs b/src/lang/vn.rs index dadc0a154369..b6c2c66c18b8 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -638,7 +638,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Set PIN", ""), ("Enable trusted devices", ""), ("Manage trusted devices", ""), - ("Trust this device", "Tin thiết bị này"), ("Platform", ""), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), From 0651ad492f24e7b953f4b153c726b55cb265c465 Mon Sep 17 00:00:00 2001 From: flusheDData <116861809+flusheDData@users.noreply.github.com> Date: Tue, 13 Aug 2024 08:11:36 +0200 Subject: [PATCH 092/541] Update es.rs (#9045) New translations --- src/lang/es.rs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/lang/es.rs b/src/lang/es.rs index 4ed1a2d62d2b..8ed8c031a0ff 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -629,17 +629,17 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("enable-bot-desc", "1, Abre un chat con @BotFather.\n2, Envía el comando \"/newbot\". Recibirás un token tras completar esta paso.\n3, Inicia un chat con tu bot recién creado. Envía un mensaje que comience con una barra (\"/\") como \"/hola\" para activarlo.\n"), ("cancel-2fa-confirm-tip", "¿Seguro que quieres cancelar 2FA?"), ("cancel-bot-confirm-tip", "¿Seguro que quieres cancelar el bot de Telegram?"), - ("About RustDesk", ""), - ("Send clipboard keystrokes", ""), + ("About RustDesk", "Acerca de RustDesk"), + ("Send clipboard keystrokes", "Enviar pulsaciones de teclas"), ("network_error_tip", ""), - ("Unlock with PIN", ""), - ("Requires at least {} characters", ""), - ("Wrong PIN", ""), - ("Set PIN", ""), - ("Enable trusted devices", ""), - ("Manage trusted devices", ""), - ("Platform", ""), - ("Days remaining", ""), + ("Unlock with PIN", "Desbloquear con PIN"), + ("Requires at least {} characters", "Se necesitan al menos {} caracteres"), + ("Wrong PIN", "PIN erróneo"), + ("Set PIN", "Establecer PIN"), + ("Enable trusted devices", "Habilitar dispositivos de confianza"), + ("Manage trusted devices", "Gestionar dispositivos de confianza"), + ("Platform", "Plataforma"), + ("Days remaining", "Días restantes"), ("enable-trusted-devices-tip", ""), ].iter().cloned().collect(); } From dbd195a46e00d19b25667fa74e325709ce11ccc3 Mon Sep 17 00:00:00 2001 From: bovirus <1262554+bovirus@users.noreply.github.com> Date: Tue, 13 Aug 2024 08:22:08 +0200 Subject: [PATCH 093/541] Update Italian language (#9047) --- src/lang/it.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/lang/it.rs b/src/lang/it.rs index ae814fe360a8..db224085b2f1 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -636,10 +636,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Requires at least {} characters", "Richiede almeno {} caratteri"), ("Wrong PIN", "PIN errato"), ("Set PIN", "Imposta PIN"), - ("Enable trusted devices", ""), - ("Manage trusted devices", ""), - ("Platform", ""), - ("Days remaining", ""), - ("enable-trusted-devices-tip", ""), + ("Enable trusted devices", "Abilita dispositivi attendibili"), + ("Manage trusted devices", "Gestisci dispositivi attendibili"), + ("Platform", "Piattaforma"), + ("Days remaining", "Giorni rimanenti"), + ("enable-trusted-devices-tip", "Salta verifica 2FA nei dispositivi attendibili"), ].iter().cloned().collect(); } From 65318efd676206d569ad304c8f4eb1112456ae25 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Tue, 13 Aug 2024 21:42:04 +0800 Subject: [PATCH 094/541] update README --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index df8c1ca3483a..8e2d9adb5b91 100644 --- a/README.md +++ b/README.md @@ -59,19 +59,19 @@ Please download Sciter dynamic library yourself. ```sh sudo apt install -y zip g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev \ libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake make \ - libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev + libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libpam0g-dev ``` ### openSUSE Tumbleweed ```sh -sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel +sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel pam-devel ``` ### Fedora 28 (CentOS 8) ```sh -sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel +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) From b477aded0b42b2acd59872fc2f486963ad18c780 Mon Sep 17 00:00:00 2001 From: Mr-Update <37781396+Mr-Update@users.noreply.github.com> Date: Wed, 14 Aug 2024 04:39:05 +0200 Subject: [PATCH 095/541] Update de.rs (#9054) --- src/lang/de.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/lang/de.rs b/src/lang/de.rs index 3e36b246a43a..9732fb70fe4a 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -636,10 +636,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Requires at least {} characters", "Erfordert mindestens {} Zeichen"), ("Wrong PIN", "Falsche PIN"), ("Set PIN", "PIN festlegen"), - ("Enable trusted devices", ""), - ("Manage trusted devices", ""), - ("Platform", ""), - ("Days remaining", ""), - ("enable-trusted-devices-tip", ""), + ("Enable trusted devices", "Vertrauenswürdige Geräte aktivieren"), + ("Manage trusted devices", "Vertrauenswürdige Geräte verwalten"), + ("Platform", "Plattform"), + ("Days remaining", "Verbleibende Tage"), + ("enable-trusted-devices-tip", "2FA-Verifizierung auf vertrauenswürdigen Geräten überspringen"), ].iter().cloned().collect(); } From f6ab5cdcb297285b3f5a45d582ae780fbe58a7a6 Mon Sep 17 00:00:00 2001 From: RustDesk <71636191+rustdesk@users.noreply.github.com> Date: Wed, 14 Aug 2024 17:32:05 +0800 Subject: [PATCH 096/541] Update config.toml (#9057) --- .cargo/config.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.cargo/config.toml b/.cargo/config.toml index 0e9fd44a90db..994fc3df5fc4 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,7 +1,7 @@ [target.x86_64-pc-windows-msvc] rustflags = ["-Ctarget-feature=+crt-static"] [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")'] rustflags = [ "-C", "link-args=-sectcreate __CGPreLoginApp __cgpreloginapp /dev/null", From 07cf1b4db5ef2f925efd3b16b87c33ce03c94809 Mon Sep 17 00:00:00 2001 From: 21pages Date: Wed, 14 Aug 2024 18:09:47 +0800 Subject: [PATCH 097/541] fix wallpaper get sid (#9058) Signed-off-by: 21pages --- src/platform/windows.rs | 38 ++------------------------------------ 1 file changed, 2 insertions(+), 36 deletions(-) diff --git a/src/platform/windows.rs b/src/platform/windows.rs index e11f7d110ec9..4495af538a7b 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -2370,34 +2370,6 @@ fn get_license() -> Option { Some(lic) } -fn get_sid_of_user(username: &str) -> ResultType { - let mut output = Command::new("wmic") - .args(&[ - "useraccount", - "where", - &format!("name='{}'", username), - "get", - "sid", - "/value", - ]) - .creation_flags(CREATE_NO_WINDOW) - .stdout(Stdio::piped()) - .spawn()? - .stdout - .ok_or_else(|| io::Error::new(io::ErrorKind::Other, "Failed to open stdout"))?; - let mut result = String::new(); - output.read_to_string(&mut result)?; - let sid_start_index = result - .find('=') - .map(|i| i + 1) - .ok_or(anyhow!("bad output format"))?; - if sid_start_index > 0 && sid_start_index < result.len() + 1 { - Ok(result[sid_start_index..].trim().to_string()) - } else { - bail!("bad output format"); - } -} - pub struct WallPaperRemover { old_path: String, } @@ -2433,12 +2405,7 @@ impl WallPaperRemover { // https://www.makeuseof.com/find-desktop-wallpapers-file-location-windows-11/ // https://superuser.com/questions/1218413/write-to-current-users-registry-through-a-different-admin-account let (hkcu, sid) = if is_root() { - let username = get_active_username(); - if username.is_empty() { - bail!("failed to get username"); - } - let sid = get_sid_of_user(&username)?; - log::info!("username: {username}, sid: {sid}"); + let sid = get_current_process_session_id().ok_or(anyhow!("failed to get sid"))?; (RegKey::predef(HKEY_USERS), format!("{}\\", sid)) } else { (RegKey::predef(HKEY_CURRENT_USER), "".to_string()) @@ -2612,8 +2579,7 @@ pub mod reg_display_settings { new: (Vec, isize), } - pub fn read_reg_connectivity() -> ResultType>> - { + pub fn read_reg_connectivity() -> ResultType>> { let hklm = winreg::RegKey::predef(HKEY_LOCAL_MACHINE); let reg_connectivity = hklm.open_subkey_with_flags( format!("{}\\{}", REG_GRAPHICS_DRIVERS_PATH, REG_CONNECTIVITY_PATH), From 5e22a49e490b4618c94375ab8c20438e5e2b255f Mon Sep 17 00:00:00 2001 From: flusheDData <116861809+flusheDData@users.noreply.github.com> Date: Thu, 15 Aug 2024 03:24:42 +0200 Subject: [PATCH 098/541] Translation correction (#9064) * Update es.rs New translations * Update es.rs Translation correction of 'Version' --- src/lang/es.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lang/es.rs b/src/lang/es.rs index 8ed8c031a0ff..ca73b8f1bae3 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -48,7 +48,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Privacy Statement", "Declaración de privacidad"), ("Mute", "Silenciar"), ("Build Date", "Fecha de compilación"), - ("Version", "¨Versión"), + ("Version", "Versión"), ("Home", "Inicio"), ("Audio Input", "Entrada de audio"), ("Enhancements", "Mejoras"), From cc9b7e64eb75d11839a7e2081500b3b8c202fff1 Mon Sep 17 00:00:00 2001 From: jkh0kr Date: Thu, 15 Aug 2024 10:25:03 +0900 Subject: [PATCH 099/541] Update ko.rs (#9067) Additional Korean translations --- src/lang/ko.rs | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 841f5f7062b2..192845b98e07 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -341,11 +341,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("General", "일반"), ("Security", "보안"), ("Theme", "테마"), - ("Dark Theme", "다크 테마"), - ("Light Theme", "라이트 테마"), - ("Dark", "다크"), - ("Light", "라이트"), - ("Follow System", "시스템 설정에따름"), + ("Dark Theme", "어두운 테마"), + ("Light Theme", "밝은 테마"), + ("Dark", "어둡게"), + ("Light", "밝게"), + ("Follow System", "시스템 기본값"), ("Enable hardware codec", "하드웨어 코덱 활성화"), ("Unlock Security Settings", "보안 설정 잠금 해제"), ("Enable audio", "오디오 활성화"), @@ -598,7 +598,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("share_warning_tip", "위의 항목들은 다른 사람들과 공유되며, 다른 사람들이 볼 수 있습니다."), ("Everyone", "모두"), ("ab_web_console_tip", "웹 콘솔에 대해 더 알아보기"), - ("allow-only-conn-window-open-tip", "RustDesk 창이 열려 있는 경우에만 연결을 허용하십시오."), + ("allow-only-conn-window-open-tip", "RustDesk 창이 열려 있는 경우에만 연결 허용"), ("no_need_privacy_mode_no_physical_displays_tip", "물리적 디스플레이가 없으므로 프라이버시 모드를 사용할 필요가 없습니다."), ("Follow remote cursor", "원격 커서 따르기"), ("Follow remote window focus", "원격 창 포커스 따르기"), @@ -632,14 +632,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About RustDesk", "RustDesk 대하여"), ("Send clipboard keystrokes", "클립보드 키 입력 전송"), ("network_error_tip", "네트워크 연결을 확인한 후 다시 시도하세요."), - ("Unlock with PIN", ""), - ("Requires at least {} characters", ""), - ("Wrong PIN", ""), - ("Set PIN", ""), - ("Enable trusted devices", ""), - ("Manage trusted devices", ""), - ("Platform", ""), - ("Days remaining", ""), - ("enable-trusted-devices-tip", ""), + ("Unlock with PIN", "핀으로 잠금 해제"), + ("Requires at least {} characters", "최소 {}자 이상 필요합니다."), + ("Wrong PIN", "잘못된 핀"), + ("Set PIN", "핀 설정"), + ("Enable trusted devices", "신뢰할 수 있는 장치 활성화"), + ("Manage trusted devices", "신뢰할 수 있는 장치 관리"), + ("Platform", "플랫폼"), + ("Days remaining", "일 남음"), + ("enable-trusted-devices-tip", "신뢰할 수 있는 기기에서 2FA 검증 건너뛰기"), ].iter().cloned().collect(); } From 85ae3916cb7a4d772db979813739a8b58c2de577 Mon Sep 17 00:00:00 2001 From: 21pages Date: Thu, 15 Aug 2024 09:57:12 +0800 Subject: [PATCH 100/541] fix get_custom_server_from_string, `relay=` is not used (#9069) Signed-off-by: 21pages --- src/custom_server.rs | 42 ++++++++++++++++++++++++++++-------------- 1 file changed, 28 insertions(+), 14 deletions(-) diff --git a/src/custom_server.rs b/src/custom_server.rs index 58d34d853884..c2c5e7f63042 100644 --- a/src/custom_server.rs +++ b/src/custom_server.rs @@ -59,30 +59,30 @@ pub fn get_custom_server_from_string(s: &str) -> ResultType { if s.contains("host=") { let stripped = &s[s.find("host=").unwrap_or(0)..s.len()]; let strs: Vec<&str> = stripped.split(",").collect(); - let mut host = ""; - let mut key = ""; - let mut api = ""; - let mut relay = ""; + let mut host = String::default(); + let mut key = String::default(); + let mut api = String::default(); + let mut relay = String::default(); let strs_iter = strs.iter(); for el in strs_iter { if el.starts_with("host=") { - host = &el[5..el.len()]; + host = el.chars().skip(5).collect(); } if el.starts_with("key=") { - key = &el[4..el.len()]; + key = el.chars().skip(4).collect(); } if el.starts_with("api=") { - api = &el[4..el.len()]; + api = el.chars().skip(4).collect(); } if el.starts_with("relay=") { - relay = &el[4..el.len()]; + relay = el.chars().skip(6).collect(); } } return Ok(CustomServer { - host: host.to_owned(), - key: key.to_owned(), - api: api.to_owned(), - relay: relay.to_owned(), + host, + key, + api, + relay, }); } else { let s = s @@ -146,8 +146,10 @@ mod test { } ); assert_eq!( - get_custom_server_from_string("rustdesk-host=server.example.net,key=Zm9vYmFyLiwyCg==,.exe") - .unwrap(), + get_custom_server_from_string( + "rustdesk-host=server.example.net,key=Zm9vYmFyLiwyCg==,.exe" + ) + .unwrap(), CustomServer { host: "server.example.net".to_owned(), key: "Zm9vYmFyLiwyCg==".to_owned(), @@ -155,6 +157,18 @@ mod test { relay: "".to_owned(), } ); + assert_eq!( + get_custom_server_from_string( + "rustdesk-host=server.example.net,key=Zm9vYmFyLiwyCg==,relay=server.example.net.exe" + ) + .unwrap(), + CustomServer { + host: "server.example.net".to_owned(), + key: "Zm9vYmFyLiwyCg==".to_owned(), + api: "".to_owned(), + relay: "server.example.net".to_owned(), + } + ); let lic = CustomServer { host: "1.1.1.1".to_owned(), key: "5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=".to_owned(), From a31c27be7316197f09a8091391ad43ef418429cd Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Thu, 15 Aug 2024 13:24:42 +0800 Subject: [PATCH 101/541] fix: windows, remote window, resizing, #9061 (#9062) Signed-off-by: fufesou --- flutter/pubspec.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index eb8426572ca4..bbd91b045ced 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -335,7 +335,7 @@ packages: description: path: "." ref: HEAD - resolved-ref: cd323a66d0771aeea4bc243a203eacfb6f89e3e4 + resolved-ref: "80b063b9d4e015f62e17f42a5aa0b3d20a365926" url: "https://github.com/rustdesk-org/rustdesk_desktop_multi_window" source: git version: "0.1.0" From bb1b9858d500e88a77649dd39dd5cd27c1feb573 Mon Sep 17 00:00:00 2001 From: 21pages Date: Thu, 15 Aug 2024 13:25:59 +0800 Subject: [PATCH 102/541] only main window show tabbar border, change cm tabbar color (#9073) Signed-off-by: 21pages --- flutter/lib/desktop/pages/server_page.dart | 10 ++++++---- .../lib/desktop/widgets/tabbar_widget.dart | 20 ++++++++++++++----- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/flutter/lib/desktop/pages/server_page.dart b/flutter/lib/desktop/pages/server_page.dart index a052437479b7..d2588a286fef 100644 --- a/flutter/lib/desktop/pages/server_page.dart +++ b/flutter/lib/desktop/pages/server_page.dart @@ -83,7 +83,7 @@ class _DesktopServerPageState extends State child: Consumer( builder: (context, serverModel, child) { final body = Scaffold( - backgroundColor: Theme.of(context).scaffoldBackgroundColor, + backgroundColor: Theme.of(context).colorScheme.background, body: ConnectionManager(), ); return isLinux @@ -193,8 +193,6 @@ class ConnectionManagerState extends State maxLabelWidth: 100, tail: null, //buildScrollJumper(), blockTab: allowRemoteCMModification() ? null : _block, - selectedTabBackgroundColor: - Theme.of(context).hintColor.withOpacity(0), tabBuilder: (key, icon, label, themeConf) { final client = serverModel.clients .firstWhereOrNull((client) => client.id.toString() == key); @@ -229,7 +227,7 @@ class ConnectionManagerState extends State borderWidth; final realChatPageWidth = constrains.maxWidth - realClosedWidth; - return Row(children: [ + final row = Row(children: [ if (constrains.maxWidth > kConnectionManagerWindowSizeClosedChat.width) Consumer( @@ -247,6 +245,10 @@ class ConnectionManagerState extends State child: SizedBox(width: realClosedWidth, child: pageView)), ]); + return Container( + color: Theme.of(context).scaffoldBackgroundColor, + child: row, + ); }, ), ), diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index fca6efb2e9ba..0b48b3b92fc3 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -505,17 +505,20 @@ class _DesktopTabState extends State Obx(() { if (stateGlobal.showTabBar.isTrue && !(kUseCompatibleUiMode && isHideSingleItem())) { + final showBottomDivider = _showTabBarBottomDivider(tabType); return SizedBox( height: _kTabBarHeight, child: Column( children: [ SizedBox( - height: _kTabBarHeight - 1, + height: + showBottomDivider ? _kTabBarHeight - 1 : _kTabBarHeight, child: _buildBar(), ), - const Divider( - height: 1, - ), + if (showBottomDivider) + const Divider( + height: 1, + ), ], ), ); @@ -1161,7 +1164,10 @@ class _TabState extends State<_Tab> with RestorationMixin { child: Row( children: [ SizedBox( - height: _kTabBarHeight, + // _kTabBarHeight also displays normally + height: _showTabBarBottomDivider(widget.tabType) + ? _kTabBarHeight - 1 + : _kTabBarHeight, child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ @@ -1414,6 +1420,10 @@ class _TabDropDownButtonState extends State<_TabDropDownButton> { } } +bool _showTabBarBottomDivider(DesktopTabType tabType) { + return tabType == DesktopTabType.main || tabType == DesktopTabType.install; +} + class TabbarTheme extends ThemeExtension { final Color? selectedTabIconColor; final Color? unSelectedTabIconColor; From dde3cce12023c82f8db333d514381dbf53e13dc3 Mon Sep 17 00:00:00 2001 From: kibeb <14143176+kibeb@users.noreply.github.com> Date: Thu, 15 Aug 2024 08:55:44 +0200 Subject: [PATCH 103/541] Update cs.rs: typo corrected (#9074) --- src/lang/cs.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lang/cs.rs b/src/lang/cs.rs index 2531a263c67c..1dff85e44670 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -160,7 +160,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Create desktop icon", "Vytvořit ikonu na ploše"), ("agreement_tip", "Spuštěním instalace přijímáte licenční ujednání."), ("Accept and Install", "Přijmout a nainstalovat"), - ("End-user license agreement", "Licencenční ujednání s koncovým uživatelem"), + ("End-user license agreement", "Licenční ujednání s koncovým uživatelem"), ("Generating ...", "Vytváření ..."), ("Your installation is lower version.", "Máte nainstalovanou starší verzi"), ("not_close_tcp_tip", "Po dobu, po kterou tunel potřebujete, nezavírejte toto okno"), From 071f51cf6f3b1aece1b2235d58a540fd2f8a509f Mon Sep 17 00:00:00 2001 From: kibeb <14143176+kibeb@users.noreply.github.com> Date: Thu, 15 Aug 2024 11:34:39 +0200 Subject: [PATCH 104/541] Update cs.rs: more corrections, new phrases translated (#9075) --- src/lang/cs.rs | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/lang/cs.rs b/src/lang/cs.rs index 1dff85e44670..7ec337b1aa60 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -37,7 +37,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is empty", "Schránka je prázdná"), ("Stop service", "Zastavit službu"), ("Change ID", "Změnit ID"), - ("Your new ID", "Váše nové ID"), + ("Your new ID", "Vaše nové ID"), ("length %min% to %max%", "délka mezi %min% a %max%"), ("starts with a letter", "začíná písmenem"), ("allowed characters", "povolené znaky"), @@ -263,7 +263,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Canvas Move", "Posun zobrazení"), ("Pinch to Zoom", "Přiblížíte roztažením dvěma prsty"), ("Canvas Zoom", "Přiblížení zobrazení"), - ("Reset canvas", "Vrátit měřtko zobrazení na výchozí"), + ("Reset canvas", "Vrátit měřítko zobrazení na výchozí"), ("No permission of file transfer", "Žádné oprávnění k přenosu souborů"), ("Note", "Poznámka"), ("Connection", "Připojení"), @@ -297,7 +297,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Succeeded", "Úspěšný"), ("Someone turns on privacy mode, exit", "Někdo zapne režim ochrany soukromí, ukončete ho"), ("Unsupported", "Nepodporováno"), - ("Peer denied", "Protistana odmítnula"), + ("Peer denied", "Protistrana odmítla"), ("Please install plugins", "Nainstalujte si prosím pluginy"), ("Peer exit", "Ukončení protistrany"), ("Failed to turn off", "Nepodařilo se vypnout"), @@ -329,7 +329,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Settings", "Nastavení obrazovky"), ("Ratio", "Poměr"), ("Image Quality", "Kvalita obrazu"), - ("Scroll Style", "Štýl posúvania"), + ("Scroll Style", "Styl posouvání"), ("Show Toolbar", "Zobrazit panel nástrojů"), ("Hide Toolbar", "Skrýt panel nástrojů"), ("Direct Connection", "Přímé spojení"), @@ -458,7 +458,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Minimize", "Minimalizovat"), ("Maximize", "Maximalizovat"), ("Your Device", "Vaše zařízení"), - ("empty_recent_tip", "Ups, žádná nedávná relace!\nČas naplánovat nové."), + ("empty_recent_tip", "Jejda, žádná nedávná relace!\nČas naplánovat novou."), ("empty_favorite_tip", "Ještě nemáte oblíbené protistrany?\nNajděte někoho, s kým se můžete spojit, a přidejte si ho do oblíbených!"), ("empty_lan_tip", "Ale ne, vypadá, že jsme ještě neobjevili žádné protistrany."), ("empty_address_book_tip", "Ach bože, zdá se, že ve vašem adresáři nejsou v současné době uvedeni žádní kolegové."), @@ -586,7 +586,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("powered_by_me", "Poháněno společností RustDesk"), ("outgoing_only_desk_tip", "Toto je přizpůsobená edice.\nMůžete se připojit k jiným zařízením, ale jiná zařízení se k vašemu zařízení připojit nemohou."), ("preset_password_warning", "Tato upravená edice je dodávána s přednastaveným heslem. Každý, kdo zná toto heslo, může získat plnou kontrolu nad vaším zařízením. Pokud jste to nečekali, okamžitě software odinstalujte."), - ("Security Alert", "Bezbečnostní Výstraha"), + ("Security Alert", "Bezpečnostní výstraha"), ("My address book", "Můj adresář"), ("Personal", "Osobní"), ("Owner", "Majitel"), @@ -632,14 +632,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About RustDesk", "O RustDesk"), ("Send clipboard keystrokes", "Odesílat stisky kláves schránky"), ("network_error_tip", "Zkontrolujte prosím připojení k síti a klikněte na tlačítko Opakovat."), - ("Unlock with PIN", ""), - ("Requires at least {} characters", ""), - ("Wrong PIN", ""), - ("Set PIN", ""), - ("Enable trusted devices", ""), - ("Manage trusted devices", ""), - ("Platform", ""), - ("Days remaining", ""), - ("enable-trusted-devices-tip", ""), + ("Unlock with PIN", "Odemknout PINem"), + ("Requires at least {} characters", "Vyžadováno aspoň {} znaků"), + ("Wrong PIN", "Nesprávný PIN"), + ("Set PIN", "Nastavit PIN"), + ("Enable trusted devices", "Povolit důvěryhodná zařízení"), + ("Manage trusted devices", "Spravovat důvěryhodná zařízení"), + ("Platform", "Platforma"), + ("Days remaining", "Zbývajících dnů"), + ("enable-trusted-devices-tip", "Přeskočte 2FA ověření na důvěryhodných zařízeních"), ].iter().cloned().collect(); } From 92752765ba0e1ec6f3398622ac3c05681eb920f0 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Thu, 15 Aug 2024 23:58:19 +0800 Subject: [PATCH 105/541] fix: resize top edge (#9081) Signed-off-by: fufesou --- flutter/lib/common.dart | 19 ++++++++++++++++++- flutter/lib/consts.dart | 6 +++--- .../lib/desktop/pages/connection_page.dart | 5 +++-- .../lib/desktop/pages/desktop_tab_page.dart | 1 + .../desktop/pages/file_manager_tab_page.dart | 1 + flutter/lib/desktop/pages/install_page.dart | 1 + .../desktop/pages/port_forward_tab_page.dart | 1 + .../lib/desktop/pages/remote_tab_page.dart | 1 + flutter/lib/models/state_model.dart | 4 ++-- 9 files changed, 31 insertions(+), 8 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 45c78f3d92d8..078bda8a420d 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -3492,7 +3492,8 @@ Widget buildVirtualWindowFrame(BuildContext context, Widget child) { ); } -get windowEdgeSize => isLinux && !_linuxWindowResizable ? 0.0 : kWindowEdgeSize; +get windowResizeEdgeSize => + isLinux && !_linuxWindowResizable ? 0.0 : kWindowResizeEdgeSize; // `windowManager.setResizable(false)` will reset the window size to the default size on Linux and then set unresizable. // See _linuxWindowResizable for more details. @@ -3570,3 +3571,19 @@ Widget netWorkErrorWidget() { ], )); } + +List? get windowManagerEnableResizeEdges => isWindows + ? [ + ResizeEdge.topLeft, + ResizeEdge.top, + ResizeEdge.topRight, + ] + : null; + +List? get subWindowManagerEnableResizeEdges => isWindows + ? [ + SubWindowResizeEdge.topLeft, + SubWindowResizeEdge.top, + SubWindowResizeEdge.topRight, + ] + : null; diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index b836200a4347..a5414dd0db02 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -241,9 +241,9 @@ const kDefaultScrollDuration = Duration(milliseconds: 50); const kDefaultMouseWheelThrottleDuration = Duration(milliseconds: 50); const kFullScreenEdgeSize = 0.0; const kMaximizeEdgeSize = 0.0; -// Do not use kWindowEdgeSize directly. Use `windowEdgeSize` in `common.dart` instead. -final kWindowEdgeSize = isWindows ? 1.0 : 5.0; -final kWindowBorderWidth = 1.0; +// Do not use kWindowResizeEdgeSize directly. Use `windowResizeEdgeSize` in `common.dart` instead. +const kWindowResizeEdgeSize = 5.0; +const kWindowBorderWidth = 1.0; const kDesktopMenuPadding = EdgeInsets.only(left: 12.0, right: 3.0); const kFrameBorderRadius = 12.0; const kFrameClipRRectBorderRadius = 12.0; diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index 09ec3418bf3c..b2073ae4a573 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -257,8 +257,9 @@ class _ConnectionPageState extends State @override void onWindowLeaveFullScreen() { // Restore edge border to default edge size. - stateGlobal.resizeEdgeSize.value = - stateGlobal.isMaximized.isTrue ? kMaximizeEdgeSize : windowEdgeSize; + stateGlobal.resizeEdgeSize.value = stateGlobal.isMaximized.isTrue + ? kMaximizeEdgeSize + : windowResizeEdgeSize; } @override diff --git a/flutter/lib/desktop/pages/desktop_tab_page.dart b/flutter/lib/desktop/pages/desktop_tab_page.dart index 2e577e625ae0..7319f7a3c158 100644 --- a/flutter/lib/desktop/pages/desktop_tab_page.dart +++ b/flutter/lib/desktop/pages/desktop_tab_page.dart @@ -126,6 +126,7 @@ class _DesktopTabPageState extends State : Obx( () => DragToResizeArea( resizeEdgeSize: stateGlobal.resizeEdgeSize.value, + enableResizeEdges: windowManagerEnableResizeEdges, child: tabWidget, ), ); diff --git a/flutter/lib/desktop/pages/file_manager_tab_page.dart b/flutter/lib/desktop/pages/file_manager_tab_page.dart index a68e4feecdc3..ca17ac3ff066 100644 --- a/flutter/lib/desktop/pages/file_manager_tab_page.dart +++ b/flutter/lib/desktop/pages/file_manager_tab_page.dart @@ -111,6 +111,7 @@ class _FileManagerTabPageState extends State { : SubWindowDragToResizeArea( child: tabWidget, resizeEdgeSize: stateGlobal.resizeEdgeSize.value, + enableResizeEdges: subWindowManagerEnableResizeEdges, windowId: stateGlobal.windowId, ); } diff --git a/flutter/lib/desktop/pages/install_page.dart b/flutter/lib/desktop/pages/install_page.dart index 5285fc35f2bf..0ff04240b554 100644 --- a/flutter/lib/desktop/pages/install_page.dart +++ b/flutter/lib/desktop/pages/install_page.dart @@ -43,6 +43,7 @@ class _InstallPageState extends State { Widget build(BuildContext context) { return DragToResizeArea( resizeEdgeSize: stateGlobal.resizeEdgeSize.value, + enableResizeEdges: windowManagerEnableResizeEdges, child: Container( child: Scaffold( backgroundColor: Theme.of(context).colorScheme.background, diff --git a/flutter/lib/desktop/pages/port_forward_tab_page.dart b/flutter/lib/desktop/pages/port_forward_tab_page.dart index 5534db85549b..812f7aa99ade 100644 --- a/flutter/lib/desktop/pages/port_forward_tab_page.dart +++ b/flutter/lib/desktop/pages/port_forward_tab_page.dart @@ -127,6 +127,7 @@ class _PortForwardTabPageState extends State { () => SubWindowDragToResizeArea( child: tabWidget, resizeEdgeSize: stateGlobal.resizeEdgeSize.value, + enableResizeEdges: subWindowManagerEnableResizeEdges, windowId: stateGlobal.windowId, ), ); diff --git a/flutter/lib/desktop/pages/remote_tab_page.dart b/flutter/lib/desktop/pages/remote_tab_page.dart index 94535bc6dcfc..dc0153da0f43 100644 --- a/flutter/lib/desktop/pages/remote_tab_page.dart +++ b/flutter/lib/desktop/pages/remote_tab_page.dart @@ -228,6 +228,7 @@ class _ConnectionTabPageState extends State { // Specially configured for a better resize area and remote control. childPadding: kDragToResizeAreaPadding, resizeEdgeSize: stateGlobal.resizeEdgeSize.value, + enableResizeEdges: subWindowManagerEnableResizeEdges, windowId: stateGlobal.windowId, )); } diff --git a/flutter/lib/models/state_model.dart b/flutter/lib/models/state_model.dart index 3c514aaaadc5..7c4d3cfd0596 100644 --- a/flutter/lib/models/state_model.dart +++ b/flutter/lib/models/state_model.dart @@ -14,7 +14,7 @@ class StateGlobal { bool _isMinimized = false; final RxBool isMaximized = false.obs; final RxBool _showTabBar = true.obs; - final RxDouble _resizeEdgeSize = RxDouble(windowEdgeSize); + final RxDouble _resizeEdgeSize = RxDouble(windowResizeEdgeSize); final RxDouble _windowBorderWidth = RxDouble(kWindowBorderWidth); final RxBool showRemoteToolBar = false.obs; final svcStatus = SvcStatus.notReady.obs; @@ -93,7 +93,7 @@ class StateGlobal { ? kFullScreenEdgeSize : isMaximized.isTrue ? kMaximizeEdgeSize - : windowEdgeSize; + : windowResizeEdgeSize; String getInputSource({bool force = false}) { if (force || _inputSource.isEmpty) { From 579e0fac3632892197edf8fba684ee9d93ec09e6 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Fri, 16 Aug 2024 12:20:40 +0800 Subject: [PATCH 106/541] fix https://github.com/rustdesk/rustdesk/issues/9088 --- flutter/lib/main.dart | 6 +++--- flutter/lib/web/bridge.dart | 2 +- flutter/macos/Runner/Info.plist | 2 ++ src/flutter_ffi.rs | 2 +- src/platform/macos.rs | 1 - src/tray.rs | 1 - 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index 0386d63cf50f..c2009bcae13a 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -260,7 +260,7 @@ showCmWindow({bool isStartup = false}) async { WindowOptions windowOptions = getHiddenTitleBarWindowOptions( size: kConnectionManagerWindowSizeClosedChat, alwaysOnTop: true); await windowManager.waitUntilReadyToShow(windowOptions, null); - bind.mainHideDocker(); + bind.mainHideDock(); await Future.wait([ windowManager.show(), windowManager.focus(), @@ -288,14 +288,14 @@ hideCmWindow({bool isStartup = false}) async { size: kConnectionManagerWindowSizeClosedChat); windowManager.setOpacity(0); await windowManager.waitUntilReadyToShow(windowOptions, null); - bind.mainHideDocker(); + bind.mainHideDock(); await windowManager.minimize(); await windowManager.hide(); _isCmReadyToShow = true; } else if (_isCmReadyToShow) { if (await windowManager.getOpacity() != 0) { await windowManager.setOpacity(0); - bind.mainHideDocker(); + bind.mainHideDock(); await windowManager.minimize(); await windowManager.hide(); } diff --git a/flutter/lib/web/bridge.dart b/flutter/lib/web/bridge.dart index 3f3846e08811..457911458ce1 100644 --- a/flutter/lib/web/bridge.dart +++ b/flutter/lib/web/bridge.dart @@ -1415,7 +1415,7 @@ class RustdeskImpl { return false; } - bool mainHideDocker({dynamic hint}) { + bool mainHideDock({dynamic hint}) { throw UnimplementedError(); } diff --git a/flutter/macos/Runner/Info.plist b/flutter/macos/Runner/Info.plist index d8f9db36ce61..36ca81e18c05 100644 --- a/flutter/macos/Runner/Info.plist +++ b/flutter/macos/Runner/Info.plist @@ -45,5 +45,7 @@ Record the sound from microphone for the purpose of the remote desktop. NSPrincipalClass NSApplication + LSUIElement + 1 diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 60f62e102bf3..b64fd6044f2a 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -1891,7 +1891,7 @@ pub fn main_is_login_wayland() -> SyncReturn { SyncReturn(is_login_wayland()) } -pub fn main_hide_docker() -> SyncReturn { +pub fn main_hide_dock() -> SyncReturn { #[cfg(target_os = "macos")] crate::platform::macos::hide_dock(); SyncReturn(true) diff --git a/src/platform/macos.rs b/src/platform/macos.rs index a6887c2791a2..3d14485d2694 100644 --- a/src/platform/macos.rs +++ b/src/platform/macos.rs @@ -493,7 +493,6 @@ pub fn lock_screen() { } pub fn start_os_service() { - crate::platform::macos::hide_dock(); log::info!("Username: {}", crate::username()); let mut sys = System::new(); let path = diff --git a/src/tray.rs b/src/tray.rs index 3a879801e486..74c18bf7bd0b 100644 --- a/src/tray.rs +++ b/src/tray.rs @@ -12,7 +12,6 @@ pub fn start_tray() { if crate::ui_interface::get_builtin_option(hbb_common::config::keys::OPTION_HIDE_TRAY) == "Y" { #[cfg(target_os = "macos")] { - crate::platform::macos::hide_dock(); loop { std::thread::sleep(std::time::Duration::from_secs(1)); } From ed18e3c7860edca81d07794773bd86ea51e14e18 Mon Sep 17 00:00:00 2001 From: 21pages Date: Fri, 16 Aug 2024 12:55:58 +0800 Subject: [PATCH 107/541] file rename (#9089) Signed-off-by: 21pages --- .../lib/desktop/pages/file_manager_page.dart | 57 ++++++++++++ flutter/lib/desktop/pages/server_page.dart | 10 +++ flutter/lib/desktop/widgets/menu_button.dart | 1 + .../lib/mobile/pages/file_manager_page.dart | 89 ++++++++++++------- flutter/lib/mobile/pages/settings_page.dart | 2 +- flutter/lib/models/cm_file_model.dart | 71 +++++++++++---- flutter/lib/models/file_model.dart | 79 ++++++++++++++++ libs/hbb_common/protos/message.proto | 7 ++ libs/hbb_common/src/fs.rs | 15 ++++ src/client.rs | 1 + src/client/file_trait.rs | 20 ++++- src/client/io_loop.rs | 19 ++++ src/flutter_ffi.rs | 12 +++ src/ipc.rs | 5 ++ src/lang/ar.rs | 3 + src/lang/be.rs | 3 + src/lang/bg.rs | 3 + src/lang/ca.rs | 3 + src/lang/cn.rs | 3 + src/lang/cs.rs | 3 + src/lang/da.rs | 3 + src/lang/de.rs | 3 + src/lang/el.rs | 3 + src/lang/eo.rs | 3 + src/lang/es.rs | 3 + src/lang/et.rs | 3 + src/lang/eu.rs | 3 + src/lang/fa.rs | 3 + src/lang/fr.rs | 3 + src/lang/he.rs | 3 + src/lang/hr.rs | 3 + src/lang/hu.rs | 3 + src/lang/id.rs | 3 + src/lang/it.rs | 3 + src/lang/ja.rs | 3 + src/lang/ko.rs | 3 + src/lang/kz.rs | 3 + src/lang/lt.rs | 3 + src/lang/lv.rs | 3 + src/lang/nb.rs | 3 + src/lang/nl.rs | 3 + src/lang/pl.rs | 3 + src/lang/pt_PT.rs | 3 + src/lang/ptbr.rs | 3 + src/lang/ro.rs | 3 + src/lang/ru.rs | 3 + src/lang/sk.rs | 3 + src/lang/sl.rs | 3 + src/lang/sq.rs | 3 + src/lang/sr.rs | 3 + src/lang/sv.rs | 3 + src/lang/template.rs | 3 + src/lang/th.rs | 3 + src/lang/tr.rs | 3 + src/lang/tw.rs | 3 + src/lang/uk.rs | 3 + src/lang/vn.rs | 3 + src/server/connection.rs | 24 +++++ src/ui_cm_interface.rs | 14 +++ 59 files changed, 506 insertions(+), 49 deletions(-) diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index 3b4428f99e4f..c9e565fd7769 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -262,6 +262,7 @@ class _FileManagerPageState extends State Offstage( offstage: item.state != JobState.paused, child: MenuButton( + tooltip: translate("Resume"), onPressed: () { jobController.resumeJob(item.id); }, @@ -274,6 +275,7 @@ class _FileManagerPageState extends State ), ), MenuButton( + tooltip: translate("Delete"), padding: EdgeInsets.only(right: 15), child: SvgPicture.asset( "assets/close.svg", @@ -521,6 +523,7 @@ class _FileManagerViewState extends State { Row( children: [ MenuButton( + tooltip: translate('Back'), padding: EdgeInsets.only( right: 3, ), @@ -540,6 +543,7 @@ class _FileManagerViewState extends State { }, ), MenuButton( + tooltip: translate('Parent directory'), child: RotatedBox( quarterTurns: 3, child: SvgPicture.asset( @@ -604,6 +608,7 @@ class _FileManagerViewState extends State { switch (_locationStatus.value) { case LocationStatus.bread: return MenuButton( + tooltip: translate('Search'), onPressed: () { _locationStatus.value = LocationStatus.fileSearchBar; Future.delayed( @@ -630,6 +635,7 @@ class _FileManagerViewState extends State { ); case LocationStatus.fileSearchBar: return MenuButton( + tooltip: translate('Clear'), onPressed: () { onSearchText("", isLocal); _locationStatus.value = LocationStatus.bread; @@ -645,6 +651,7 @@ class _FileManagerViewState extends State { } }), MenuButton( + tooltip: translate('Refresh File'), padding: EdgeInsets.only( left: 3, ), @@ -670,6 +677,7 @@ class _FileManagerViewState extends State { isLocal ? MainAxisAlignment.start : MainAxisAlignment.end, children: [ MenuButton( + tooltip: translate('Home'), padding: EdgeInsets.only( right: 3, ), @@ -685,11 +693,27 @@ class _FileManagerViewState extends State { hoverColor: Theme.of(context).hoverColor, ), MenuButton( + tooltip: translate('Create Folder'), onPressed: () { final name = TextEditingController(); + String? errorText; _ffi.dialogManager.show((setState, close, context) { + name.addListener(() { + if (errorText != null) { + setState(() { + errorText = null; + }); + } + }); submit() { 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.directory.value.path, name.value.text, @@ -721,6 +745,7 @@ class _FileManagerViewState extends State { labelText: translate( "Please enter the folder name", ), + errorText: errorText, ), controller: name, autofocus: true, @@ -754,6 +779,7 @@ class _FileManagerViewState extends State { hoverColor: Theme.of(context).hoverColor, ), Obx(() => MenuButton( + tooltip: translate('Delete'), onPressed: SelectedItems.valid(selectedItems.items) ? () async { await (controller @@ -885,6 +911,7 @@ class _FileManagerViewState extends State { menuPos = RelativeRect.fromLTRB(x, y, x, y); }, child: MenuButton( + tooltip: translate('More'), onPressed: () => mod_menu.showMenu( context: context, position: menuPos, @@ -974,6 +1001,7 @@ class _FileManagerViewState extends State { final lastModifiedStr = entry.isDrive ? " " : "${entry.lastModified().toString().replaceAll(".000", "")} "; + var secondaryPosition = RelativeRect.fromLTRB(0, 0, 0, 0); return Padding( padding: EdgeInsets.symmetric(vertical: 1), child: Obx(() => Container( @@ -1038,6 +1066,35 @@ class _FileManagerViewState extends State { _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("Rename"), + height: CustomPopupMenuTheme.height, + onTap: () { + controller.renameAction(entry, isLocal); + }, + ) + ]; + if (items.isNotEmpty) { + mod_menu.showMenu( + context: context, + position: secondaryPosition, + items: items, + ); + } + }, + onSecondaryTapDown: (details) { + secondaryPosition = RelativeRect.fromLTRB( + details.globalPosition.dx, + details.globalPosition.dy, + details.globalPosition.dx, + details.globalPosition.dy); + }, ), SizedBox( width: 2.0, diff --git a/flutter/lib/desktop/pages/server_page.dart b/flutter/lib/desktop/pages/server_page.dart index d2588a286fef..53ba73c972f4 100644 --- a/flutter/lib/desktop/pages/server_page.dart +++ b/flutter/lib/desktop/pages/server_page.dart @@ -1157,6 +1157,16 @@ class __FileTransferLogPageState extends State<_FileTransferLogPage> { Text(translate('Create Folder')) ], ); + case CmFileAction.rename: + return Column( + children: [ + Icon( + Icons.drive_file_move_outlined, + color: Theme.of(context).tabBarTheme.labelColor, + ), + Text(translate('Rename')) + ], + ); } } diff --git a/flutter/lib/desktop/widgets/menu_button.dart b/flutter/lib/desktop/widgets/menu_button.dart index 17b160fed0ac..8fc90de114b9 100644 --- a/flutter/lib/desktop/widgets/menu_button.dart +++ b/flutter/lib/desktop/widgets/menu_button.dart @@ -34,6 +34,7 @@ class _MenuButtonState extends State { return Padding( padding: widget.padding, child: Tooltip( + waitDuration: Duration(milliseconds: 300), message: widget.tooltip, child: Material( type: MaterialType.transparency, diff --git a/flutter/lib/mobile/pages/file_manager_page.dart b/flutter/lib/mobile/pages/file_manager_page.dart index b74d44484fc2..e017b5b6faec 100644 --- a/flutter/lib/mobile/pages/file_manager_page.dart +++ b/flutter/lib/mobile/pages/file_manager_page.dart @@ -204,36 +204,54 @@ class _FileManagerPageState extends State { setState(() {}); } else if (v == "folder") { final name = TextEditingController(); - gFFI.dialogManager - .show((setState, close, context) => CustomAlertDialog( - title: Text(translate("Create Folder")), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - TextFormField( - decoration: InputDecoration( - labelText: translate( - "Please enter the folder name"), - ), - controller: name, - ), - ], + String? errorText; + gFFI.dialogManager.show((setState, close, context) { + name.addListener(() { + if (errorText != null) { + setState(() { + errorText = null; + }); + } + }); + return CustomAlertDialog( + title: Text(translate("Create Folder")), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextFormField( + decoration: InputDecoration( + labelText: + translate("Please enter the folder name"), + errorText: errorText, ), - actions: [ - dialogButton("Cancel", - onPressed: () => close(false), - isOutline: true), - dialogButton("OK", onPressed: () { - if (name.value.text.isNotEmpty) { - currentFileController.createDir( - PathUtil.join( - currentDir.path, - name.value.text, - currentOptions.isWindows)); - close(); - } - }) - ])); + controller: name, + ), + ], + ), + actions: [ + dialogButton("Cancel", + onPressed: () => close(false), isOutline: true), + dialogButton("OK", onPressed: () { + if (name.value.text.isNotEmpty) { + if (!PathUtil.validName( + name.value.text, + currentFileController + .options.value.isWindows)) { + setState(() { + errorText = + translate("Invalid folder name"); + }); + return; + } + currentFileController.createDir(PathUtil.join( + currentDir.path, + name.value.text, + currentOptions.isWindows)); + close(); + } + }) + ]); + }); } else if (v == "hidden") { currentFileController.toggleShowHidden(); } @@ -497,7 +515,15 @@ class _FileManagerViewState extends State { child: Text(translate("Properties")), value: "properties", enabled: false, - ) + ), + if (!entries[index].isDrive && + versionCmp(gFFI.ffiModel.pi.version, + "1.3.0") >= + 0) + PopupMenuItem( + child: Text(translate("Rename")), + value: "rename", + ) ]; }, onSelected: (v) { @@ -509,6 +535,9 @@ class _FileManagerViewState extends State { _selectedItems.clear(); widget.selectMode.toggle(isLocal); setState(() {}); + } else if (v == "rename") { + controller.renameAction( + entries[index], isLocal); } }), onTap: () { diff --git a/flutter/lib/mobile/pages/settings_page.dart b/flutter/lib/mobile/pages/settings_page.dart index ca1ca1afa478..8fac2ea2a040 100644 --- a/flutter/lib/mobile/pages/settings_page.dart +++ b/flutter/lib/mobile/pages/settings_page.dart @@ -291,7 +291,7 @@ class _SettingsState extends State with WidgetsBindingObserver { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(translate('Enable trusted devices')), - Text(translate('enable-trusted-devices-tip'), + Text('* ${translate('enable-trusted-devices-tip')}', style: Theme.of(context).textTheme.bodySmall), ], ), diff --git a/flutter/lib/models/cm_file_model.dart b/flutter/lib/models/cm_file_model.dart index ce9b711a2782..6609f1191c3c 100644 --- a/flutter/lib/models/cm_file_model.dart +++ b/flutter/lib/models/cm_file_model.dart @@ -33,6 +33,8 @@ class CmFileModel { _onFileRemove(evt['remove']); } else if (evt['create_dir'] != null) { _onDirCreate(evt['create_dir']); + } else if (evt['rename'] != null) { + _onRename(evt['rename']); } } @@ -59,8 +61,6 @@ class CmFileModel { _dealOneJob(dynamic l, bool calcSpeed) { final data = TransferJobSerdeData.fromJson(l); - Client? client = - gFFI.serverModel.clients.firstWhereOrNull((e) => e.id == data.connId); var jobTable = _jobTables[data.connId]; if (jobTable == null) { debugPrint("jobTable should not be null"); @@ -70,12 +70,7 @@ class CmFileModel { if (job == null) { job = CmFileLog(); jobTable.add(job); - final currentSelectedTab = - gFFI.serverModel.tabController.state.value.selectedTabInfo; - if (!(gFFI.chatModel.isShowCMSidePage && - currentSelectedTab.key == data.connId.toString())) { - client?.unreadChatMessageCount.value += 1; - } + _addUnread(data.connId); } job.id = data.id; job.action = @@ -167,8 +162,6 @@ class CmFileModel { try { dynamic d = jsonDecode(log); FileActionLog data = FileActionLog.fromJson(d); - Client? client = - gFFI.serverModel.clients.firstWhereOrNull((e) => e.id == data.connId); var jobTable = _jobTables[data.connId]; if (jobTable == null) { debugPrint("jobTable should not be null"); @@ -179,17 +172,45 @@ class CmFileModel { ..fileName = data.path ..action = CmFileAction.createDir ..state = JobState.done); - final currentSelectedTab = - gFFI.serverModel.tabController.state.value.selectedTabInfo; - if (!(gFFI.chatModel.isShowCMSidePage && - currentSelectedTab.key == data.connId.toString())) { - client?.unreadChatMessageCount.value += 1; + _addUnread(data.connId); + jobTable.refresh(); + } catch (e) { + debugPrint('$e'); + } + } + + _onRename(dynamic log) { + try { + dynamic d = jsonDecode(log); + FileRenamenLog data = FileRenamenLog.fromJson(d); + var jobTable = _jobTables[data.connId]; + if (jobTable == null) { + debugPrint("jobTable should not be null"); + return; } + final fileName = '${data.path} -> ${data.newName}'; + jobTable.add(CmFileLog() + ..id = 0 + ..fileName = fileName + ..action = CmFileAction.rename + ..state = JobState.done); + _addUnread(data.connId); jobTable.refresh(); } catch (e) { debugPrint('$e'); } } + + _addUnread(int connId) { + Client? client = + gFFI.serverModel.clients.firstWhereOrNull((e) => e.id == connId); + final currentSelectedTab = + gFFI.serverModel.tabController.state.value.selectedTabInfo; + if (!(gFFI.chatModel.isShowCMSidePage && + currentSelectedTab.key == connId.toString())) { + client?.unreadChatMessageCount.value += 1; + } + } } enum CmFileAction { @@ -198,6 +219,7 @@ enum CmFileAction { localToRemote, remove, createDir, + rename, } class CmFileLog { @@ -285,3 +307,22 @@ class FileActionLog { dir: d['dir'] ?? false, ); } + +class FileRenamenLog { + int connId = 0; + String path = ''; + String newName = ''; + + FileRenamenLog({ + required this.connId, + required this.path, + required this.newName, + }); + + FileRenamenLog.fromJson(dynamic d) + : this( + connId: d['connId'] ?? 0, + path: d['path'] ?? '', + newName: d['newName'] ?? '', + ); +} diff --git a/flutter/lib/models/file_model.dart b/flutter/lib/models/file_model.dart index c32ec54056ed..0838c8b06738 100644 --- a/flutter/lib/models/file_model.dart +++ b/flutter/lib/models/file_model.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/common/widgets/dialog.dart'; import 'package:flutter_hbb/utils/event_loop.dart'; import 'package:get/get.dart'; import 'package:path/path.dart' as path; @@ -642,6 +643,77 @@ class FileController { path: path, isRemote: !isLocal); } + + Future renameAction(Entry item, bool isLocal) async { + final textEditingController = TextEditingController(text: item.name); + String? errorText; + dialogManager?.show((setState, close, context) { + textEditingController.addListener(() { + if (errorText != null) { + setState(() { + errorText = null; + }); + } + }); + submit() async { + final newName = textEditingController.text; + if (newName.isEmpty || newName == item.name) { + close(); + return; + } + if (directory.value.entries.any((e) => e.name == newName)) { + setState(() { + errorText = translate("Already exists"); + }); + return; + } + if (!PathUtil.validName(newName, options.value.isWindows)) { + setState(() { + if (item.isDirectory) { + errorText = translate("Invalid folder name"); + } else { + errorText = translate("Invalid file name"); + } + }); + return; + } + await bind.sessionRenameFile( + sessionId: sessionId, + actId: JobController.jobID.next(), + path: item.path, + newName: newName, + isRemote: !isLocal); + close(); + } + + return CustomAlertDialog( + content: Column( + children: [ + DialogTextField( + title: '${translate('Rename')} ${item.name}', + controller: textEditingController, + errorText: errorText, + ), + ], + ), + actions: [ + dialogButton( + "Cancel", + icon: Icon(Icons.close_rounded), + onPressed: close, + isOutline: true, + ), + dialogButton( + "OK", + icon: Icon(Icons.done_rounded), + onPressed: submit, + ), + ], + onSubmit: submit, + onCancel: close, + ); + }); + } } class JobController { @@ -1083,6 +1155,13 @@ class PathUtil { final pathUtil = isWindows ? windowsContext : posixContext; return pathUtil.dirname(path); } + + static bool validName(String name, bool isWindows) { + final unixFileNamePattern = RegExp(r'^[^/\0]+$'); + final windowsFileNamePattern = RegExp(r'^[^<>:"/\\|?*]+$'); + final reg = isWindows ? windowsFileNamePattern : unixFileNamePattern; + return reg.hasMatch(name); + } } class DirectoryOptions { diff --git a/libs/hbb_common/protos/message.proto b/libs/hbb_common/protos/message.proto index 497bdee9ba8b..4554617a7d61 100644 --- a/libs/hbb_common/protos/message.proto +++ b/libs/hbb_common/protos/message.proto @@ -371,6 +371,12 @@ message ReadAllFiles { bool include_hidden = 3; } +message FileRename { + int32 id = 1; + string path = 2; + string new_name = 3; +} + message FileAction { oneof union { ReadDir read_dir = 1; @@ -382,6 +388,7 @@ message FileAction { ReadAllFiles all_files = 7; FileTransferCancel cancel = 8; FileTransferSendConfirmRequest send_confirm = 9; + FileRename rename = 10; } } diff --git a/libs/hbb_common/src/fs.rs b/libs/hbb_common/src/fs.rs index 235cb4837b16..3f236fd3ae38 100644 --- a/libs/hbb_common/src/fs.rs +++ b/libs/hbb_common/src/fs.rs @@ -838,6 +838,21 @@ pub fn create_dir(dir: &str) -> ResultType<()> { Ok(()) } +#[inline] +pub fn rename_file(path: &str, new_name: &str) -> ResultType<()> { + let path = std::path::Path::new(&path); + if path.exists() { + let dir = path + .parent() + .ok_or(anyhow!("Parent directoy of {path:?} not exists"))?; + let new_path = dir.join(&new_name); + std::fs::rename(&path, &new_path)?; + Ok(()) + } else { + bail!("{path:?} not exists"); + } +} + #[inline] pub fn transform_windows_path(entries: &mut Vec) { for entry in entries { diff --git a/src/client.rs b/src/client.rs index 2c5d0a3399e9..665560d62155 100644 --- a/src/client.rs +++ b/src/client.rs @@ -3180,6 +3180,7 @@ pub enum Data { NewVoiceCall, CloseVoiceCall, ResetDecoder(Option), + RenameFile((i32, String, String, bool)), } /// Keycode for key events. diff --git a/src/client/file_trait.rs b/src/client/file_trait.rs index 49e3f23585fb..71ddfb09cf4b 100644 --- a/src/client/file_trait.rs +++ b/src/client/file_trait.rs @@ -1,4 +1,4 @@ -use hbb_common::{fs, message_proto::*, log}; +use hbb_common::{fs, log, message_proto::*}; use super::{Data, Interface}; @@ -7,7 +7,12 @@ pub trait FileManager: Interface { fs::get_home_as_string() } - #[cfg(not(any(target_os = "android", target_os = "ios", feature = "cli", feature = "flutter")))] + #[cfg(not(any( + target_os = "android", + target_os = "ios", + feature = "cli", + feature = "flutter" + )))] fn read_dir(&self, path: String, include_hidden: bool) -> sciter::Value { match fs::read_dir(&fs::get_path(&path), include_hidden) { Err(_) => sciter::Value::null(), @@ -20,7 +25,12 @@ pub trait FileManager: Interface { } } - #[cfg(any(target_os = "android", target_os = "ios", feature = "cli", feature = "flutter"))] + #[cfg(any( + target_os = "android", + target_os = "ios", + feature = "cli", + feature = "flutter" + ))] fn read_dir(&self, path: &str, include_hidden: bool) -> String { use crate::common::make_fd_to_json; match fs::read_dir(&fs::get_path(path), include_hidden) { @@ -136,4 +146,8 @@ pub trait FileManager: Interface { is_upload, ))); } + + fn rename_file(&self, act_id: i32, path: String, new_name: String, is_remote: bool) { + self.send(Data::RenameFile((act_id, path, new_name, is_remote))); + } } diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index 15c6c24b537f..b222e411815c 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -817,6 +817,25 @@ impl Remote { } } } + Data::RenameFile((id, path, new_name, is_remote)) => { + if is_remote { + let mut msg_out = Message::new(); + let mut file_action = FileAction::new(); + file_action.set_rename(FileRename { + id, + path, + new_name, + ..Default::default() + }); + msg_out.set_file_action(file_action); + allow_err!(peer.send(&msg_out).await); + } else { + let err = fs::rename_file(&path, &new_name) + .err() + .map(|e| e.to_string()); + self.handle_job_status(id, -1, err); + } + } Data::RecordScreen(start, display, w, h, id) => { let _ = self .video_sender diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index b64fd6044f2a..9b9914cfd0fb 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -710,6 +710,18 @@ pub fn session_resume_job(session_id: SessionID, act_id: i32, is_remote: bool) { } } +pub fn session_rename_file( + session_id: SessionID, + act_id: i32, + path: String, + new_name: String, + is_remote: bool, +) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + session.rename_file(act_id, path, new_name, is_remote); + } +} + pub fn session_elevate_direct(session_id: SessionID) { if let Some(session) = sessions::get_session_by_session_id(&session_id) { session.elevate_direct(); diff --git a/src/ipc.rs b/src/ipc.rs index 7903a942b4f5..3f093c758e2d 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -102,6 +102,11 @@ pub enum FS { last_modified: u64, is_upload: bool, }, + Rename { + id: i32, + path: String, + new_name: String, + }, } #[cfg(target_os = "windows")] diff --git a/src/lang/ar.rs b/src/lang/ar.rs index d1f6d35e3274..68c041481e5f 100644 --- a/src/lang/ar.rs +++ b/src/lang/ar.rs @@ -641,5 +641,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Platform", ""), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), + ("Parent directory", ""), + ("Resume", ""), + ("Invalid file name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/be.rs b/src/lang/be.rs index 6bf449df220c..136ccf9feec5 100644 --- a/src/lang/be.rs +++ b/src/lang/be.rs @@ -641,5 +641,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Platform", ""), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), + ("Parent directory", ""), + ("Resume", ""), + ("Invalid file name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/bg.rs b/src/lang/bg.rs index dfdfaa85d61f..9a5b320a85d0 100644 --- a/src/lang/bg.rs +++ b/src/lang/bg.rs @@ -641,5 +641,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Platform", ""), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), + ("Parent directory", ""), + ("Resume", ""), + ("Invalid file name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ca.rs b/src/lang/ca.rs index 4a98333fd337..0aa64ec28e69 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -641,5 +641,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Platform", ""), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), + ("Parent directory", ""), + ("Resume", ""), + ("Invalid file name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 0768bd25e599..62cb5452c601 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -641,5 +641,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Platform", "平台"), ("Days remaining", "剩余天数"), ("enable-trusted-devices-tip", "允许受信任的设备跳过 2FA 验证"), + ("Parent directory", "父目录"), + ("Resume", "继续"), + ("Invalid file name", "无效文件名"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index 7ec337b1aa60..41d94d97846a 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -641,5 +641,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Platform", "Platforma"), ("Days remaining", "Zbývajících dnů"), ("enable-trusted-devices-tip", "Přeskočte 2FA ověření na důvěryhodných zařízeních"), + ("Parent directory", ""), + ("Resume", ""), + ("Invalid file name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index 11d6516b57cd..a9e286600b66 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -641,5 +641,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Platform", ""), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), + ("Parent directory", ""), + ("Resume", ""), + ("Invalid file name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index 9732fb70fe4a..677f58552fef 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -641,5 +641,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Platform", "Plattform"), ("Days remaining", "Verbleibende Tage"), ("enable-trusted-devices-tip", "2FA-Verifizierung auf vertrauenswürdigen Geräten überspringen"), + ("Parent directory", ""), + ("Resume", ""), + ("Invalid file name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/el.rs b/src/lang/el.rs index e5331c104277..b3ce7dcaf80c 100644 --- a/src/lang/el.rs +++ b/src/lang/el.rs @@ -641,5 +641,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Platform", ""), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), + ("Parent directory", ""), + ("Resume", ""), + ("Invalid file name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index ffa4d3b7de4a..144bf7bc3c45 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -641,5 +641,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Platform", ""), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), + ("Parent directory", ""), + ("Resume", ""), + ("Invalid file name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index ca73b8f1bae3..afac3f5584f6 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -641,5 +641,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Platform", "Plataforma"), ("Days remaining", "Días restantes"), ("enable-trusted-devices-tip", ""), + ("Parent directory", ""), + ("Resume", ""), + ("Invalid file name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/et.rs b/src/lang/et.rs index 70c834a6cd2a..cc3f3afc3922 100644 --- a/src/lang/et.rs +++ b/src/lang/et.rs @@ -641,5 +641,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Platform", ""), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), + ("Parent directory", ""), + ("Resume", ""), + ("Invalid file name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eu.rs b/src/lang/eu.rs index 3bb756859330..412b4e74060c 100644 --- a/src/lang/eu.rs +++ b/src/lang/eu.rs @@ -641,5 +641,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Platform", ""), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), + ("Parent directory", ""), + ("Resume", ""), + ("Invalid file name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index 898596a15e95..b6949aa50436 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -641,5 +641,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Platform", ""), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), + ("Parent directory", ""), + ("Resume", ""), + ("Invalid file name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index c0eefab3639c..31d78640888d 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -641,5 +641,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Platform", ""), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), + ("Parent directory", ""), + ("Resume", ""), + ("Invalid file name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/he.rs b/src/lang/he.rs index 45fefe666894..07d9aa977d17 100644 --- a/src/lang/he.rs +++ b/src/lang/he.rs @@ -641,5 +641,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Platform", ""), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), + ("Parent directory", ""), + ("Resume", ""), + ("Invalid file name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hr.rs b/src/lang/hr.rs index 599b87c7b7b8..1dca1c7e0c05 100644 --- a/src/lang/hr.rs +++ b/src/lang/hr.rs @@ -641,5 +641,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Platform", ""), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), + ("Parent directory", ""), + ("Resume", ""), + ("Invalid file name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index ef8d0d1931c5..be089347aca7 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -641,5 +641,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Platform", ""), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), + ("Parent directory", ""), + ("Resume", ""), + ("Invalid file name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index f814a5c21c2b..42c36b5c744e 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -641,5 +641,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Platform", ""), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), + ("Parent directory", ""), + ("Resume", ""), + ("Invalid file name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index db224085b2f1..281704f5eff2 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -641,5 +641,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Platform", "Piattaforma"), ("Days remaining", "Giorni rimanenti"), ("enable-trusted-devices-tip", "Salta verifica 2FA nei dispositivi attendibili"), + ("Parent directory", ""), + ("Resume", ""), + ("Invalid file name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 27736008ca70..ffb93e379f78 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -641,5 +641,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Platform", ""), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), + ("Parent directory", ""), + ("Resume", ""), + ("Invalid file name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 192845b98e07..3e84ca262f54 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -641,5 +641,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Platform", "플랫폼"), ("Days remaining", "일 남음"), ("enable-trusted-devices-tip", "신뢰할 수 있는 기기에서 2FA 검증 건너뛰기"), + ("Parent directory", ""), + ("Resume", ""), + ("Invalid file name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index 7fe5b8b38d64..c47764c8190f 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -641,5 +641,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Platform", ""), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), + ("Parent directory", ""), + ("Resume", ""), + ("Invalid file name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lt.rs b/src/lang/lt.rs index 922bb1881a0c..b1d0317f89d0 100644 --- a/src/lang/lt.rs +++ b/src/lang/lt.rs @@ -641,5 +641,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Platform", ""), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), + ("Parent directory", ""), + ("Resume", ""), + ("Invalid file name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lv.rs b/src/lang/lv.rs index 8b00c360ee37..df2324def573 100644 --- a/src/lang/lv.rs +++ b/src/lang/lv.rs @@ -641,5 +641,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Platform", ""), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), + ("Parent directory", ""), + ("Resume", ""), + ("Invalid file name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nb.rs b/src/lang/nb.rs index 0ce11a845e8f..d23191ef2b73 100644 --- a/src/lang/nb.rs +++ b/src/lang/nb.rs @@ -641,5 +641,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Platform", ""), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), + ("Parent directory", ""), + ("Resume", ""), + ("Invalid file name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nl.rs b/src/lang/nl.rs index 2873fb474dfd..a6b681a14bb2 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -641,5 +641,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Platform", ""), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), + ("Parent directory", ""), + ("Resume", ""), + ("Invalid file name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 5b8061c5936e..246de02f4a78 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -641,5 +641,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Platform", ""), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), + ("Parent directory", ""), + ("Resume", ""), + ("Invalid file name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index 1ec66daee4bc..6afa4c528eb4 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -641,5 +641,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Platform", ""), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), + ("Parent directory", ""), + ("Resume", ""), + ("Invalid file name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index 8983413f5691..83ee1e0d2339 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -641,5 +641,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Platform", ""), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), + ("Parent directory", ""), + ("Resume", ""), + ("Invalid file name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ro.rs b/src/lang/ro.rs index b913fdac3507..0f11e5449534 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -641,5 +641,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Platform", ""), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), + ("Parent directory", ""), + ("Resume", ""), + ("Invalid file name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 965bc0ce6f9e..dc4b2133f9eb 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -641,5 +641,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Platform", ""), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), + ("Parent directory", ""), + ("Resume", ""), + ("Invalid file name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 5c3247374a3c..d1f5467afaa0 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -641,5 +641,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Platform", ""), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), + ("Parent directory", ""), + ("Resume", ""), + ("Invalid file name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sl.rs b/src/lang/sl.rs index 0973fdac807d..7c8d97494867 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -641,5 +641,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Platform", ""), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), + ("Parent directory", ""), + ("Resume", ""), + ("Invalid file name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sq.rs b/src/lang/sq.rs index bf89fd05f11e..0a73e021744d 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -641,5 +641,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Platform", ""), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), + ("Parent directory", ""), + ("Resume", ""), + ("Invalid file name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sr.rs b/src/lang/sr.rs index 987b88e4f432..4d3654ea9987 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -641,5 +641,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Platform", ""), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), + ("Parent directory", ""), + ("Resume", ""), + ("Invalid file name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sv.rs b/src/lang/sv.rs index d079cdfbdb6d..9d7956545bcf 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -641,5 +641,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Platform", ""), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), + ("Parent directory", ""), + ("Resume", ""), + ("Invalid file name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index e11bb0663d31..76e491c91ce7 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -641,5 +641,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Platform", ""), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), + ("Parent directory", ""), + ("Resume", ""), + ("Invalid file name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/th.rs b/src/lang/th.rs index 0ab7963481a5..acd14c8f9c56 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -641,5 +641,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Platform", ""), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), + ("Parent directory", ""), + ("Resume", ""), + ("Invalid file name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index 98f07122ffcc..f926e94549a2 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -641,5 +641,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Platform", ""), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), + ("Parent directory", ""), + ("Resume", ""), + ("Invalid file name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index d935b2dae20c..99c808320957 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -641,5 +641,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Platform", ""), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), + ("Parent directory", ""), + ("Resume", ""), + ("Invalid file name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/uk.rs b/src/lang/uk.rs index 6465299f7494..6a177059cfa2 100644 --- a/src/lang/uk.rs +++ b/src/lang/uk.rs @@ -641,5 +641,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Platform", ""), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), + ("Parent directory", ""), + ("Resume", ""), + ("Invalid file name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index b6c2c66c18b8..88f70a8e2c4d 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -641,5 +641,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Platform", ""), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), + ("Parent directory", ""), + ("Resume", ""), + ("Invalid file name", ""), ].iter().cloned().collect(); } diff --git a/src/server/connection.rs b/src/server/connection.rs index 929d040e2413..3d05a938b16e 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -2274,6 +2274,22 @@ impl Connection { job.confirm(&r); } } + Some(file_action::Union::Rename(r)) => { + self.send_fs(ipc::FS::Rename { + id: r.id, + path: r.path.clone(), + new_name: r.new_name.clone(), + }); + self.send_to_cm(ipc::Data::FileTransferLog(( + "rename".to_string(), + serde_json::to_string(&FileRenameLog { + conn_id: self.inner.id(), + path: r.path, + new_name: r.new_name, + }) + .unwrap_or_default(), + ))); + } _ => {} } } @@ -3451,6 +3467,14 @@ struct FileActionLog { dir: bool, } +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +struct FileRenameLog { + conn_id: i32, + path: String, + new_name: String, +} + struct FileRemoveLogControl { conn_id: i32, instant: Instant, diff --git a/src/ui_cm_interface.rs b/src/ui_cm_interface.rs index 451de6b239af..89e9ceabbeae 100644 --- a/src/ui_cm_interface.rs +++ b/src/ui_cm_interface.rs @@ -881,6 +881,9 @@ async fn handle_fs( } } } + ipc::FS::Rename { id, path, new_name } => { + rename_file(path, new_name, id, tx).await; + } _ => {} } } @@ -945,6 +948,17 @@ async fn create_dir(path: String, id: i32, tx: &UnboundedSender) { .await; } +#[cfg(not(any(target_os = "ios")))] +async fn rename_file(path: String, new_name: String, id: i32, tx: &UnboundedSender) { + handle_result( + spawn_blocking(move || fs::rename_file(&path, &new_name)).await, + id, + 0, + tx, + ) + .await; +} + #[cfg(not(any(target_os = "ios")))] async fn remove_dir(path: String, id: i32, recursive: bool, tx: &UnboundedSender) { let path = fs::get_path(&path); From f31e60af5bf3cfac2d43918a0f940b162a00313d Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Fri, 16 Aug 2024 14:55:42 +0800 Subject: [PATCH 108/541] fix: crash, drop tokio RunTime in async context (#9091) * fix: crash, drop tokio RunTime in async context Signed-off-by: fufesou * chore Signed-off-by: fufesou --------- Signed-off-by: fufesou --- src/server.rs | 2 +- src/server/connection.rs | 2 +- src/server/input_service.rs | 12 +++++++ src/server/wayland.rs | 62 ++++++++++++++++++++----------------- 4 files changed, 47 insertions(+), 31 deletions(-) diff --git a/src/server.rs b/src/server.rs index 6505ad1c2d34..547886a5c989 100644 --- a/src/server.rs +++ b/src/server.rs @@ -481,7 +481,7 @@ pub async fn start_server(is_server: bool) { }); input_service::fix_key_down_timeout_loop(); #[cfg(target_os = "linux")] - if crate::platform::current_is_wayland() { + if input_service::wayland_use_uinput() { allow_err!(input_service::setup_uinput(0, 1920, 0, 1080).await); } #[cfg(any(target_os = "macos", target_os = "linux"))] diff --git a/src/server/connection.rs b/src/server/connection.rs index 3d05a938b16e..7b160ec2116f 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -1320,7 +1320,7 @@ impl Connection { #[cfg(target_os = "linux")] { // use rdp_input when uinput is not available in wayland. Ex: flatpak - if !is_x11() && !crate::is_server() { + if input_service::wayland_use_rdp_input() { let _ = setup_rdp_input().await; } } diff --git a/src/server/input_service.rs b/src/server/input_service.rs index eabb8844e04b..2f9b86480a2c 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -1636,6 +1636,18 @@ async fn send_sas() -> ResultType<()> { Ok(()) } +#[inline] +#[cfg(target_os = "linux")] +pub fn wayland_use_uinput() -> bool { + !crate::platform::is_x11() && crate::is_server() +} + +#[inline] +#[cfg(target_os = "linux")] +pub fn wayland_use_rdp_input() -> bool { + !crate::platform::is_x11() && !crate::is_server() +} + lazy_static::lazy_static! { static ref MODIFIER_MAP: HashMap = [ (ControlKey::Alt, Key::Alt), diff --git a/src/server/wayland.rs b/src/server/wayland.rs index 12939320c90f..5560cb95ed3d 100644 --- a/src/server/wayland.rs +++ b/src/server/wayland.rs @@ -135,6 +135,7 @@ pub(super) async fn check_init() -> ResultType<()> { let mut maxx = 0; let mut miny = 0; let mut maxy = 0; + let use_uinput = crate::input_service::wayland_use_uinput(); if *CAP_DISPLAY_INFO.read().unwrap() == 0 { let mut lock = CAP_DISPLAY_INFO.write().unwrap(); @@ -167,28 +168,29 @@ pub(super) async fn check_init() -> ResultType<()> { num_cpus::get(), ); - let (max_width, max_height) = match get_max_desktop_resolution() { - Some(result) if !result.is_empty() => { - let resolution: Vec<&str> = result.split(" ").collect(); - let w: i32 = resolution[0].parse().unwrap_or(origin.0 + width as i32); - let h: i32 = resolution[2] - .trim_end_matches(",") - .parse() - .unwrap_or(origin.1 + height as i32); - if w < origin.0 + width as i32 || h < origin.1 + height as i32 { - (origin.0 + width as i32, origin.1 + height as i32) + if use_uinput { + let (max_width, max_height) = match get_max_desktop_resolution() { + Some(result) if !result.is_empty() => { + let resolution: Vec<&str> = result.split(" ").collect(); + let w: i32 = resolution[0].parse().unwrap_or(origin.0 + width as i32); + let h: i32 = resolution[2] + .trim_end_matches(",") + .parse() + .unwrap_or(origin.1 + height as i32); + if w < origin.0 + width as i32 || h < origin.1 + height as i32 { + (origin.0 + width as i32, origin.1 + height as i32) + } else { + (w, h) + } } - else{ - (w, h) - } - } - _ => (origin.0 + width as i32, origin.1 + height as i32), - }; + _ => (origin.0 + width as i32, origin.1 + height as i32), + }; - minx = 0; - maxx = max_width; - miny = 0; - maxy = max_height; + minx = 0; + maxx = max_width; + miny = 0; + maxy = max_height; + } let capturer = Box::into_raw(Box::new( Capturer::new(display).with_context(|| "Failed to create capturer")?, @@ -206,15 +208,17 @@ pub(super) async fn check_init() -> ResultType<()> { } } - if minx != maxx && miny != maxy { - log::info!( - "update mouse resolution: ({}, {}), ({}, {})", - minx, - maxx, - miny, - maxy - ); - allow_err!(input_service::update_mouse_resolution(minx, maxx, miny, maxy).await); + if use_uinput { + if minx != maxx && miny != maxy { + log::info!( + "update mouse resolution: ({}, {}), ({}, {})", + minx, + maxx, + miny, + maxy + ); + allow_err!(input_service::update_mouse_resolution(minx, maxx, miny, maxy).await); + } } } Ok(()) From e1329c8157d5c6f308acee985da0af513b0d229b Mon Sep 17 00:00:00 2001 From: FastAct <93490087+FastAct@users.noreply.github.com> Date: Fri, 16 Aug 2024 09:19:18 +0200 Subject: [PATCH 109/541] Update nl.rs (#9092) --- src/lang/nl.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/lang/nl.rs b/src/lang/nl.rs index a6b681a14bb2..9ae7d0839f92 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -636,13 +636,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Requires at least {} characters", "Vereist minstens {} tekens"), ("Wrong PIN", "Verkeerde PIN-code"), ("Set PIN", "PIN-code instellen"), - ("Enable trusted devices", ""), - ("Manage trusted devices", ""), - ("Platform", ""), - ("Days remaining", ""), - ("enable-trusted-devices-tip", ""), - ("Parent directory", ""), - ("Resume", ""), - ("Invalid file name", ""), + ("Enable trusted devices", "Vertrouwde apparaten inschakelen"), + ("Manage trusted devices", "Vertrouwde apparaten beheren"), + ("Platform", "Platform"), + ("Days remaining", "Resterende dagen"), + ("enable-trusted-devices-tip", "2FA-verificatie overslaan op vertrouwde apparaten"), + ("Parent directory", "Hoofdmap"), + ("Resume", "Hervatten"), + ("Invalid file name", "Ongeldige bestandsnaam"), ].iter().cloned().collect(); } From 7744bdbbe0288c1a694e9c78421f87dff3d6f9b1 Mon Sep 17 00:00:00 2001 From: bovirus <1262554+bovirus@users.noreply.github.com> Date: Fri, 16 Aug 2024 12:39:36 +0200 Subject: [PATCH 110/541] Update Italian language (#9093) --- src/lang/it.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lang/it.rs b/src/lang/it.rs index 281704f5eff2..7c18b52d2a14 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -641,8 +641,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Platform", "Piattaforma"), ("Days remaining", "Giorni rimanenti"), ("enable-trusted-devices-tip", "Salta verifica 2FA nei dispositivi attendibili"), - ("Parent directory", ""), - ("Resume", ""), - ("Invalid file name", ""), + ("Parent directory", "Cartella principale"), + ("Resume", "Riprendi"), + ("Invalid file name", "Nome file non valido"), ].iter().cloned().collect(); } From d1fe61767076cdf3b044c2dd87351da211bf6742 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Sun, 18 Aug 2024 09:30:53 +0800 Subject: [PATCH 111/541] fixing https://github.com/rustdesk/rustdesk/issues/9103, maybe --- .cargo/config.toml | 2 ++ appimage/AppImageBuilder-aarch64.yml | 1 - appimage/AppImageBuilder-x86_64.yml | 1 - 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.cargo/config.toml b/.cargo/config.toml index 994fc3df5fc4..d0e4c2b4b266 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -6,3 +6,5 @@ rustflags = ["-C", "target-feature=+crt-static", "-C", "link-args=/NODEFAULTLIB: rustflags = [ "-C", "link-args=-sectcreate __CGPreLoginApp __cgpreloginapp /dev/null", ] +[target.'cfg(target_os="linux")'] +rustflags = ["-Ctarget-feature=+crt-static"] diff --git a/appimage/AppImageBuilder-aarch64.yml b/appimage/AppImageBuilder-aarch64.yml index c64966f28192..a7792d8a952c 100644 --- a/appimage/AppImageBuilder-aarch64.yml +++ b/appimage/AppImageBuilder-aarch64.yml @@ -37,7 +37,6 @@ AppDir: universe multiverse key_url: 'http://keyserver.ubuntu.com/pks/lookup?op=get&search=0x3b4fe6acc0b21f32' include: - - libc6:arm64 - libgtk-3-0 - libxcb-randr0 - libxdo3 diff --git a/appimage/AppImageBuilder-x86_64.yml b/appimage/AppImageBuilder-x86_64.yml index 3c0479d962f0..21277718acbc 100644 --- a/appimage/AppImageBuilder-x86_64.yml +++ b/appimage/AppImageBuilder-x86_64.yml @@ -37,7 +37,6 @@ AppDir: - sourceline: deb http://archive.ubuntu.com/ubuntu/ focal-security main restricted universe multiverse include: - - libc6:amd64 - libgtk-3-0 - libxcb-randr0 - libxdo3 From 4e084c5ee0318c4ea24aa97d44d324a3480c981c Mon Sep 17 00:00:00 2001 From: rustdesk Date: Sun, 18 Aug 2024 09:56:24 +0800 Subject: [PATCH 112/541] revert --- .cargo/config.toml | 7 +++++-- appimage/AppImageBuilder-aarch64.yml | 1 + appimage/AppImageBuilder-x86_64.yml | 3 +++ 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/.cargo/config.toml b/.cargo/config.toml index d0e4c2b4b266..6a02a103dd46 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -6,5 +6,8 @@ rustflags = ["-C", "target-feature=+crt-static", "-C", "link-args=/NODEFAULTLIB: rustflags = [ "-C", "link-args=-sectcreate __CGPreLoginApp __cgpreloginapp /dev/null", ] -[target.'cfg(target_os="linux")'] -rustflags = ["-Ctarget-feature=+crt-static"] +#[target.'cfg(target_os="linux")'] +# glibc-static required, this may fix https://github.com/rustdesk/rustdesk/issues/9103, but I do not wanna to this big change +#rustflags = [ +# "-C", "link-args=-Wl,-Bstatic -lc -Wl,-Bdynamic" +#] diff --git a/appimage/AppImageBuilder-aarch64.yml b/appimage/AppImageBuilder-aarch64.yml index a7792d8a952c..c64966f28192 100644 --- a/appimage/AppImageBuilder-aarch64.yml +++ b/appimage/AppImageBuilder-aarch64.yml @@ -37,6 +37,7 @@ AppDir: universe multiverse key_url: 'http://keyserver.ubuntu.com/pks/lookup?op=get&search=0x3b4fe6acc0b21f32' include: + - libc6:arm64 - libgtk-3-0 - libxcb-randr0 - libxdo3 diff --git a/appimage/AppImageBuilder-x86_64.yml b/appimage/AppImageBuilder-x86_64.yml index 21277718acbc..d9853c1817b0 100644 --- a/appimage/AppImageBuilder-x86_64.yml +++ b/appimage/AppImageBuilder-x86_64.yml @@ -37,6 +37,9 @@ AppDir: - sourceline: deb http://archive.ubuntu.com/ubuntu/ focal-security main restricted universe multiverse 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, but I do not wanna try. + - libc6:amd64 - libgtk-3-0 - libxcb-randr0 - libxdo3 From 0a5fafb84fa88944408aeefa84e403143febd262 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Sun, 18 Aug 2024 10:04:20 +0800 Subject: [PATCH 113/541] comment --- .cargo/config.toml | 3 ++- appimage/AppImageBuilder-x86_64.yml | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.cargo/config.toml b/.cargo/config.toml index 6a02a103dd46..d971518eb1dd 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -7,7 +7,8 @@ rustflags = [ "-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 wanna to this big change +# 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" #] diff --git a/appimage/AppImageBuilder-x86_64.yml b/appimage/AppImageBuilder-x86_64.yml index d9853c1817b0..e1b460364b34 100644 --- a/appimage/AppImageBuilder-x86_64.yml +++ b/appimage/AppImageBuilder-x86_64.yml @@ -38,7 +38,8 @@ AppDir: universe multiverse 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, but I do not wanna try. + # 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 - libgtk-3-0 - libxcb-randr0 From e3f09b3ec66c3565e63552c39f36b2a89696943f Mon Sep 17 00:00:00 2001 From: Mr-Update <37781396+Mr-Update@users.noreply.github.com> Date: Sun, 18 Aug 2024 21:12:56 +0200 Subject: [PATCH 114/541] Update de.rs (#9107) --- src/lang/de.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lang/de.rs b/src/lang/de.rs index 677f58552fef..e8e61543a48e 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -641,8 +641,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Platform", "Plattform"), ("Days remaining", "Verbleibende Tage"), ("enable-trusted-devices-tip", "2FA-Verifizierung auf vertrauenswürdigen Geräten überspringen"), - ("Parent directory", ""), - ("Resume", ""), - ("Invalid file name", ""), + ("Parent directory", "Übergeordnetes Verzeichnis"), + ("Resume", "Fortsetzen"), + ("Invalid file name", "Ungültiger Dateiname"), ].iter().cloned().collect(); } From 715d475f495b551b0adf22db675ef023e517fa0d Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Mon, 19 Aug 2024 11:35:35 +0800 Subject: [PATCH 115/541] fix: privacy mode 2, more error info (#9111) Signed-off-by: fufesou --- src/privacy_mode/win_virtual_display.rs | 38 ++++++++++++++++++------- 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/src/privacy_mode/win_virtual_display.rs b/src/privacy_mode/win_virtual_display.rs index afaafa86529a..de85e7ba8dc2 100644 --- a/src/privacy_mode/win_virtual_display.rs +++ b/src/privacy_mode/win_virtual_display.rs @@ -21,8 +21,8 @@ use winapi::{ winuser::{ ChangeDisplaySettingsExW, EnumDisplayDevicesW, EnumDisplaySettingsExW, EnumDisplaySettingsW, CDS_NORESET, CDS_RESET, CDS_SET_PRIMARY, CDS_UPDATEREGISTRY, - DISP_CHANGE_SUCCESSFUL, EDD_GET_DEVICE_INTERFACE_NAME, ENUM_CURRENT_SETTINGS, - ENUM_REGISTRY_SETTINGS, + DISP_CHANGE_FAILED, DISP_CHANGE_SUCCESSFUL, EDD_GET_DEVICE_INTERFACE_NAME, + ENUM_CURRENT_SETTINGS, ENUM_REGISTRY_SETTINGS, }, }, }; @@ -155,6 +155,19 @@ impl PrivacyModeImpl { self.virtual_displays_added.clear(); } + #[inline] + fn change_display_settings_ex_err_msg(rc: i32) -> String { + if rc != DISP_CHANGE_FAILED { + format!("ret: {}", rc) + } else { + format!( + "ret: {}, last error: {:?}", + rc, + std::io::Error::last_os_error() + ) + } + } + fn set_primary_display(&mut self) -> ResultType<()> { let display = &self.virtual_displays[0]; @@ -224,13 +237,14 @@ impl PrivacyModeImpl { NULL, ); if rc != DISP_CHANGE_SUCCESSFUL { + let err = Self::change_display_settings_ex_err_msg(rc); log::error!( - "Failed ChangeDisplaySettingsEx, device name: {:?}, flags: {}, ret: {}", + "Failed ChangeDisplaySettingsEx, device name: {:?}, flags: {}, {}", std::string::String::from_utf16(&dd.DeviceName), flags, - rc + &err ); - bail!("Failed ChangeDisplaySettingsEx, ret: {}", rc); + bail!("Failed ChangeDisplaySettingsEx, {}", err); } // If we want to set dpi, the following references may be helpful. @@ -264,13 +278,14 @@ impl PrivacyModeImpl { NULL as _, ); if rc != DISP_CHANGE_SUCCESSFUL { + let err = Self::change_display_settings_ex_err_msg(rc); log::error!( - "Failed ChangeDisplaySettingsEx, device name: {:?}, flags: {}, ret: {}", + "Failed ChangeDisplaySettingsEx, device name: {:?}, flags: {}, {}", std::string::String::from_utf16(&display.name), flags, - rc + &err ); - bail!("Failed ChangeDisplaySettingsEx, ret: {}", rc); + bail!("Failed ChangeDisplaySettingsEx, {}", err); } } } @@ -327,9 +342,10 @@ impl PrivacyModeImpl { // } // } - let ret = ChangeDisplaySettingsExW(NULL as _, NULL as _, NULL as _, flags, NULL as _); - if ret != DISP_CHANGE_SUCCESSFUL { - bail!("Failed ChangeDisplaySettingsEx, ret: {}", ret); + let rc = ChangeDisplaySettingsExW(NULL as _, NULL as _, NULL as _, flags, NULL as _); + if rc != DISP_CHANGE_SUCCESSFUL { + let err = Self::change_display_settings_ex_err_msg(rc); + bail!("Failed ChangeDisplaySettingsEx, {}", err); } // if !desk_current.is_null() { From c0de0aa108a96e1aa9a9c931efad4c5d6e93a79e Mon Sep 17 00:00:00 2001 From: XLion Date: Mon, 19 Aug 2024 14:13:31 +0800 Subject: [PATCH 116/541] Update tw.rs (#9112) --- src/lang/tw.rs | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 99c808320957..9bbef24311d6 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -632,17 +632,17 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About RustDesk", "關於 RustDesk"), ("Send clipboard keystrokes", "發送剪貼簿按鍵"), ("network_error_tip", "請檢查網路連結,然後點擊重試"), - ("Unlock with PIN", ""), - ("Requires at least {} characters", ""), - ("Wrong PIN", ""), - ("Set PIN", ""), - ("Enable trusted devices", ""), - ("Manage trusted devices", ""), - ("Platform", ""), - ("Days remaining", ""), - ("enable-trusted-devices-tip", ""), - ("Parent directory", ""), - ("Resume", ""), - ("Invalid file name", ""), + ("Unlock with PIN", "使用 PIN 碼解鎖設定"), + ("Requires at least {} characters", "不少於 {} 個字元"), + ("Wrong PIN", "PIN 碼錯誤"), + ("Set PIN", "設定 PIN 碼"), + ("Enable trusted devices", "啟用信任設備"), + ("Manage trusted devices", "管理信任設備"), + ("Platform", "平台"), + ("Days remaining", "剩餘天數"), + ("enable-trusted-devices-tip", "允許受信任的設備跳過 2FA 驗證"), + ("Parent directory", "父目錄"), + ("Resume", "繼續"), + ("Invalid file name", "無效文件名"), ].iter().cloned().collect(); } From 921b64e1e000564dcc95863c09877a38cbeea343 Mon Sep 17 00:00:00 2001 From: flusheDData <116861809+flusheDData@users.noreply.github.com> Date: Mon, 19 Aug 2024 08:14:47 +0200 Subject: [PATCH 117/541] Last tips (#9113) * Update es.rs New translations * Update es.rs Translation correction of 'Version' * Update es.rs Last tips --------- Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com> --- src/lang/es.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lang/es.rs b/src/lang/es.rs index afac3f5584f6..9371cc3e080f 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -631,7 +631,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("cancel-bot-confirm-tip", "¿Seguro que quieres cancelar el bot de Telegram?"), ("About RustDesk", "Acerca de RustDesk"), ("Send clipboard keystrokes", "Enviar pulsaciones de teclas"), - ("network_error_tip", ""), + ("network_error_tip", "Por fvor, comprueba tu conexión de red e inténtalo de nuevo.""), ("Unlock with PIN", "Desbloquear con PIN"), ("Requires at least {} characters", "Se necesitan al menos {} caracteres"), ("Wrong PIN", "PIN erróneo"), @@ -640,7 +640,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Manage trusted devices", "Gestionar dispositivos de confianza"), ("Platform", "Plataforma"), ("Days remaining", "Días restantes"), - ("enable-trusted-devices-tip", ""), + ("enable-trusted-devices-tip", "Omitir la verificación en dos fases en dispositivos de confianza"), ("Parent directory", ""), ("Resume", ""), ("Invalid file name", ""), From da70cbcddae703e7fb80e077adbd452c078c132c Mon Sep 17 00:00:00 2001 From: RustDesk <71636191+rustdesk@users.noreply.github.com> Date: Mon, 19 Aug 2024 14:19:24 +0800 Subject: [PATCH 118/541] Update es.rs --- src/lang/es.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lang/es.rs b/src/lang/es.rs index 9371cc3e080f..edf0d6175b2a 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -631,7 +631,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("cancel-bot-confirm-tip", "¿Seguro que quieres cancelar el bot de Telegram?"), ("About RustDesk", "Acerca de RustDesk"), ("Send clipboard keystrokes", "Enviar pulsaciones de teclas"), - ("network_error_tip", "Por fvor, comprueba tu conexión de red e inténtalo de nuevo.""), + ("network_error_tip", "Por fvor, comprueba tu conexión de red e inténtalo de nuevo."), ("Unlock with PIN", "Desbloquear con PIN"), ("Requires at least {} characters", "Se necesitan al menos {} caracteres"), ("Wrong PIN", "PIN erróneo"), From 2a0fd55af76cb32d1312cb0166d73ed7214f7ae1 Mon Sep 17 00:00:00 2001 From: flusheDData <116861809+flusheDData@users.noreply.github.com> Date: Mon, 19 Aug 2024 08:41:28 +0200 Subject: [PATCH 119/541] Update es.rs (#9114) New terms added --- src/lang/es.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lang/es.rs b/src/lang/es.rs index edf0d6175b2a..579d770bd500 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -641,8 +641,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Platform", "Plataforma"), ("Days remaining", "Días restantes"), ("enable-trusted-devices-tip", "Omitir la verificación en dos fases en dispositivos de confianza"), - ("Parent directory", ""), - ("Resume", ""), - ("Invalid file name", ""), + ("Parent directory", "Directorio superior"), + ("Resume", "Continuar"), + ("Invalid file name", "Nombre de archivo no válido"), ].iter().cloned().collect(); } From 8745fcbb6adc5729b3571486083c27fdd07e67ab Mon Sep 17 00:00:00 2001 From: 21pages Date: Tue, 20 Aug 2024 10:53:55 +0800 Subject: [PATCH 120/541] opt desktop file manager status list (#9117) * Show delete file/dir log * Show full path rather than base file name * Show files count * Opt status card layout * Change selected color to accent Signed-off-by: 21pages --- .../lib/desktop/pages/file_manager_page.dart | 93 ++++--- flutter/lib/models/file_model.dart | 252 +++++++++++++++--- flutter/lib/models/model.dart | 9 +- 3 files changed, 272 insertions(+), 82 deletions(-) diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index c9e565fd7769..682ffa831e8c 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -173,10 +173,25 @@ class _FileManagerPageState extends State /// transfer status list /// watch transfer status 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: job.isRemoteToLocal ? pi : 0, + child: Icon(Icons.arrow_forward_ios, color: color), + ); + } + } + statusListView(List jobs) => ListView.builder( controller: ScrollController(), itemBuilder: (BuildContext context, int index) { final item = jobs[index]; + final status = item.getStatus(); return Padding( padding: const EdgeInsets.only(bottom: 5), child: generateCard( @@ -186,15 +201,8 @@ class _FileManagerPageState extends State Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ - Transform.rotate( - angle: item.isRemoteToLocal ? pi : 0, - child: SvgPicture.asset("assets/arrow.svg", - colorFilter: svgColor( - Theme.of(context).tabBarTheme.labelColor)), - ).paddingOnly(left: 15), - const SizedBox( - width: 16.0, - ), + getIcon(item) + .marginSymmetric(horizontal: 10, vertical: 12), Expanded( child: Column( mainAxisSize: MainAxisSize.min, @@ -204,44 +212,24 @@ class _FileManagerPageState extends State waitDuration: Duration(milliseconds: 500), message: item.jobName, child: Text( - item.fileName, + item.jobName, maxLines: 1, overflow: TextOverflow.ellipsis, - ).paddingSymmetric(vertical: 10), - ), - Text( - '${translate("Total")} ${readableFileSize(item.totalSize.toDouble())}', - style: TextStyle( - fontSize: 12, - color: MyTheme.darkGray, - ), - ), - Offstage( - offstage: item.state != JobState.inProgress, - child: Text( - '${translate("Speed")} ${readableFileSize(item.speed)}/s', - style: TextStyle( - fontSize: 12, - color: MyTheme.darkGray, - ), ), ), - Offstage( - offstage: item.state == JobState.inProgress, - child: Text( - translate( - item.display(), - ), - style: TextStyle( - fontSize: 12, - color: MyTheme.darkGray, - ), - ), + Tooltip( + waitDuration: Duration(milliseconds: 500), + message: status, + child: Text(status, + style: TextStyle( + fontSize: 12, + color: MyTheme.darkGray, + )).marginOnly(top: 6), ), Offstage( - offstage: item.state != JobState.inProgress, + offstage: item.type != JobType.transfer || + item.state != JobState.inProgress, child: LinearPercentIndicator( - padding: EdgeInsets.only(right: 15), animateFromLastPercent: true, center: Text( '${(item.finishedSize / item.totalSize * 100).toStringAsFixed(0)}%', @@ -251,7 +239,7 @@ class _FileManagerPageState extends State progressColor: MyTheme.accent, backgroundColor: Theme.of(context).hoverColor, lineHeight: kDesktopFileTransferRowHeight, - ).paddingSymmetric(vertical: 15), + ).paddingSymmetric(vertical: 8), ), ], ), @@ -276,7 +264,6 @@ class _FileManagerPageState extends State ), MenuButton( tooltip: translate("Delete"), - padding: EdgeInsets.only(right: 15), child: SvgPicture.asset( "assets/close.svg", colorFilter: svgColor(Colors.white), @@ -289,11 +276,11 @@ class _FileManagerPageState extends State hoverColor: MyTheme.accent80, ), ], - ), + ).marginAll(12), ], ), ], - ).paddingSymmetric(vertical: 10), + ), ), ); }, @@ -1007,7 +994,7 @@ class _FileManagerViewState extends State { child: Obx(() => Container( decoration: BoxDecoration( color: selectedItems.items.contains(entry) - ? Theme.of(context).hoverColor + ? MyTheme.button : Theme.of(context).cardColor, borderRadius: BorderRadius.all( Radius.circular(5.0), @@ -1050,6 +1037,11 @@ class _FileManagerViewState extends State { ), Expanded( child: Text(entry.name.nonBreaking, + style: TextStyle( + color: selectedItems.items + .contains(entry) + ? Colors.white + : null), overflow: TextOverflow.ellipsis)) ]), @@ -1111,7 +1103,10 @@ class _FileManagerViewState extends State { overflow: TextOverflow.ellipsis, style: TextStyle( fontSize: 12, - color: MyTheme.darkGray, + color: selectedItems.items + .contains(entry) + ? Colors.white70 + : MyTheme.darkGray, ), )), ), @@ -1131,7 +1126,11 @@ class _FileManagerViewState extends State { sizeStr, overflow: TextOverflow.ellipsis, style: TextStyle( - fontSize: 10, color: MyTheme.darkGray), + fontSize: 10, + color: + selectedItems.items.contains(entry) + ? Colors.white70 + : MyTheme.darkGray), ), ), ), diff --git a/flutter/lib/models/file_model.dart b/flutter/lib/models/file_model.dart index 0838c8b06738..68af293804ed 100644 --- a/flutter/lib/models/file_model.dart +++ b/flutter/lib/models/file_model.dart @@ -451,7 +451,7 @@ class FileController { final isWindows = otherSideData.options.isWindows; final showHidden = otherSideData.options.showHidden; for (var from in items.items) { - final jobID = jobController.add(from, isRemoteToLocal); + final jobID = jobController.addTransferJob(from, isRemoteToLocal); bind.sessionSendFiles( sessionId: sessionId, actId: jobID, @@ -494,13 +494,21 @@ class FileController { fd.format(isWindows); dialogManager?.dismissAll(); if (fd.entries.isEmpty) { + var deleteJobId = jobController.addDeleteDirJob(item, !isLocal, 0); final confirm = await showRemoveDialog( translate( "Are you sure you want to delete this empty directory?"), item.name, false); if (confirm == true) { - sendRemoveEmptyDir(item.path, 0); + sendRemoveEmptyDir( + item.path, + 0, + deleteJobId, + ); + } else { + jobController.updateJobStatus(deleteJobId, + error: "cancel", state: JobState.done); } return; } @@ -508,6 +516,13 @@ class FileController { } else { entries = []; } + int deleteJobId; + if (item.isDirectory) { + deleteJobId = + jobController.addDeleteDirJob(item, !isLocal, entries.length); + } else { + deleteJobId = jobController.addDeleteFileJob(item, !isLocal); + } for (var i = 0; i < entries.length; i++) { final dirShow = item.isDirectory @@ -522,24 +537,32 @@ class FileController { ); try { if (confirm == true) { - sendRemoveFile(entries[i].path, i); + sendRemoveFile(entries[i].path, i, deleteJobId); final res = await jobController.jobResultListener.start(); // handle remove res; if (item.isDirectory && res['file_num'] == (entries.length - 1).toString()) { - sendRemoveEmptyDir(item.path, i); + sendRemoveEmptyDir(item.path, i, deleteJobId); } + } else { + jobController.updateJobStatus(deleteJobId, + file_num: i, error: "cancel"); } if (_removeCheckboxRemember) { if (confirm == true) { for (var j = i + 1; j < entries.length; j++) { - sendRemoveFile(entries[j].path, j); + sendRemoveFile(entries[j].path, j, deleteJobId); final res = await jobController.jobResultListener.start(); if (item.isDirectory && res['file_num'] == (entries.length - 1).toString()) { - sendRemoveEmptyDir(item.path, i); + sendRemoveEmptyDir(item.path, i, deleteJobId); } } + } else { + jobController.updateJobStatus(deleteJobId, + error: "cancel", + file_num: entries.length, + state: JobState.done); } break; } @@ -618,22 +641,19 @@ class FileController { }, useAnimation: false); } - void sendRemoveFile(String path, int fileNum) { + void sendRemoveFile(String path, int fileNum, int actId) { bind.sessionRemoveFile( sessionId: sessionId, - actId: JobController.jobID.next(), + actId: actId, path: path, isRemote: !isLocal, fileNum: fileNum); } - void sendRemoveEmptyDir(String path, int fileNum) { + void sendRemoveEmptyDir(String path, int fileNum, int actId) { history.removeWhere((element) => element.contains(path)); bind.sessionRemoveAllEmptyDirs( - sessionId: sessionId, - actId: JobController.jobID.next(), - path: path, - isRemote: !isLocal); + sessionId: sessionId, actId: actId, path: path, isRemote: !isLocal); } Future createDir(String path) async { @@ -729,14 +749,11 @@ class JobController { return jobTable.indexWhere((element) => element.id == id); } - // JobProgress? getJob(int id) { - // return jobTable.firstWhere((element) => element.id == id); - // } - // return jobID - int add(Entry from, bool isRemoteToLocal) { + int addTransferJob(Entry from, bool isRemoteToLocal) { final jobID = JobController.jobID.next(); jobTable.add(JobProgress() + ..type = JobType.transfer ..fileName = path.basename(from.path) ..jobName = from.path ..totalSize = from.size @@ -746,6 +763,33 @@ class JobController { return jobID; } + int addDeleteFileJob(Entry file, bool isRemote) { + final jobID = JobController.jobID.next(); + jobTable.add(JobProgress() + ..type = JobType.deleteFile + ..fileName = path.basename(file.path) + ..jobName = file.path + ..totalSize = file.size + ..state = JobState.none + ..id = jobID + ..isRemoteToLocal = isRemote); + return jobID; + } + + int addDeleteDirJob(Entry file, bool isRemote, int fileCount) { + final jobID = JobController.jobID.next(); + jobTable.add(JobProgress() + ..type = JobType.deleteDir + ..fileName = path.basename(file.path) + ..jobName = file.path + ..fileCount = fileCount + ..totalSize = file.size + ..state = JobState.none + ..id = jobID + ..isRemoteToLocal = isRemote); + return jobID; + } + void tryUpdateJobProgress(Map evt) { try { int id = int.parse(evt['id']); @@ -756,6 +800,7 @@ class JobController { job.fileNum = int.parse(evt['file_num']); job.speed = double.parse(evt['speed']); job.finishedSize = int.parse(evt['finished_size']); + job.recvJobRes = true; debugPrint("update job $id with $evt"); jobTable.refresh(); } @@ -764,20 +809,48 @@ class JobController { } } - void jobDone(Map evt) async { + Future jobDone(Map evt) async { if (jobResultListener.isListening) { jobResultListener.complete(evt); - return; + // return; } - - int id = int.parse(evt['id']); + int id = -1; + int? fileNum = 0; + double? speed = 0; + try { + id = int.parse(evt['id']); + } catch (_) {} final jobIndex = getJob(id); - if (jobIndex != -1) { - final job = jobTable[jobIndex]; - job.finishedSize = job.totalSize; + if (jobIndex == -1) return true; + final job = jobTable[jobIndex]; + job.recvJobRes = true; + if (job.type == JobType.deleteFile) { job.state = JobState.done; - job.fileNum = int.parse(evt['file_num']); - jobTable.refresh(); + } else if (job.type == JobType.deleteDir) { + try { + fileNum = int.tryParse(evt['file_num']); + } catch (_) {} + if (fileNum != null) { + if (fileNum < job.fileNum) return true; // file_num can be 0 at last + job.fileNum = fileNum; + if (fileNum >= job.fileCount - 1) { + job.state = JobState.done; + } + } + } else { + try { + fileNum = int.tryParse(evt['file_num']); + speed = double.tryParse(evt['speed']); + } catch (_) {} + if (fileNum != null) job.fileNum = fileNum; + if (speed != null) job.speed = speed; + job.state = JobState.done; + } + jobTable.refresh(); + if (job.type == JobType.deleteDir) { + return job.state == JobState.done; + } else { + return true; } } @@ -788,16 +861,52 @@ class JobController { final job = jobTable[jobIndex]; job.state = JobState.error; job.err = err; - job.fileNum = int.parse(evt['file_num']); - if (err == "skipped") { - job.state = JobState.done; - job.finishedSize = job.totalSize; + job.recvJobRes = true; + if (job.type == JobType.transfer) { + int? fileNum = int.tryParse(evt['file_num']); + if (fileNum != null) job.fileNum = fileNum; + if (err == "skipped") { + job.state = JobState.done; + job.finishedSize = job.totalSize; + } + } else if (job.type == JobType.deleteDir) { + if (jobResultListener.isListening) { + jobResultListener.complete(evt); + } + int? fileNum = int.tryParse(evt['file_num']); + if (fileNum != null) job.fileNum = fileNum; + } else if (job.type == JobType.deleteFile) { + if (jobResultListener.isListening) { + jobResultListener.complete(evt); + } } jobTable.refresh(); } debugPrint("jobError $evt"); } + void updateJobStatus(int id, + {int? file_num, String? error, JobState? state}) { + final jobIndex = getJob(id); + if (jobIndex < 0) return; + final job = jobTable[jobIndex]; + job.recvJobRes = true; + if (file_num != null) { + job.fileNum = file_num; + } + if (error != null) { + job.err = error; + job.state = JobState.error; + } + if (state != null) { + job.state = state; + } + if (job.type == JobType.deleteFile && error == null) { + job.state = JobState.done; + } + jobTable.refresh(); + } + Future cancelJob(int id) async { await bind.sessionCancelJob(sessionId: sessionId, actId: id); } @@ -814,6 +923,7 @@ class JobController { final currJobId = JobController.jobID.next(); String fileName = path.basename(isRemote ? remote : to); var jobProgress = JobProgress() + ..type = JobType.transfer ..fileName = fileName ..jobName = isRemote ? remote : to ..id = currJobId @@ -1088,8 +1198,12 @@ extension JobStateDisplay on JobState { } } +enum JobType { none, transfer, deleteFile, deleteDir } + class JobProgress { + JobType type = JobType.none; JobState state = JobState.none; + var recvJobRes = false; var id = 0; var fileNum = 0; var speed = 0.0; @@ -1109,7 +1223,9 @@ class JobProgress { int lastTransferredSize = 0; clear() { + type = JobType.none; state = JobState.none; + recvJobRes = false; id = 0; fileNum = 0; speed = 0; @@ -1123,11 +1239,81 @@ class JobProgress { } String display() { - if (state == JobState.done && err == "skipped") { - return translate("Skipped"); + if (type == JobType.transfer) { + if (state == JobState.done && err == "skipped") { + return translate("Skipped"); + } + } else if (type == JobType.deleteFile) { + if (err == "cancel") { + return translate("Cancel"); + } } + return state.display(); } + + String getStatus() { + int handledFileCount = recvJobRes ? fileNum + 1 : fileNum; + if (handledFileCount >= fileCount) { + handledFileCount = fileCount; + } + if (state == JobState.done) { + handledFileCount = fileCount; + finishedSize = totalSize; + } + final filesStr = "$handledFileCount/$fileCount files"; + final sizeStr = totalSize > 0 ? readableFileSize(totalSize.toDouble()) : ""; + final sizePercentStr = totalSize > 0 && finishedSize > 0 + ? "${readableFileSize(finishedSize.toDouble())} / ${readableFileSize(totalSize.toDouble())}" + : ""; + if (type == JobType.deleteFile) { + return display(); + } else if (type == JobType.deleteDir) { + var res = ''; + if (state == JobState.done || state == JobState.error) { + res = display(); + } + if (filesStr.isNotEmpty) { + if (res.isNotEmpty) { + res += " "; + } + res += filesStr; + } + + if (sizeStr.isNotEmpty) { + if (res.isNotEmpty) { + res += ", "; + } + res += sizeStr; + } + return res; + } else if (type == JobType.transfer) { + var res = ""; + if (state != JobState.inProgress && state != JobState.none) { + res += display(); + } + if (filesStr.isNotEmpty) { + if (res.isNotEmpty) { + res += ", "; + } + res += filesStr; + } + if (sizeStr.isNotEmpty && state != JobState.inProgress) { + if (res.isNotEmpty) { + res += ", "; + } + res += sizeStr; + } + if (sizePercentStr.isNotEmpty && state == JobState.inProgress) { + if (res.isNotEmpty) { + res += ", "; + } + res += sizePercentStr; + } + return res; + } + return ''; + } } class _PathStat { diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 050a92a5f641..d5377ea9a450 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -304,8 +304,13 @@ class FfiModel with ChangeNotifier { } else if (name == 'job_progress') { parent.target?.fileModel.jobController.tryUpdateJobProgress(evt); } else if (name == 'job_done') { - parent.target?.fileModel.jobController.jobDone(evt); - parent.target?.fileModel.refreshAll(); + bool? refresh = + await parent.target?.fileModel.jobController.jobDone(evt); + if (refresh == true) { + // many job done for delete directory + // todo: refresh may not work when confirm delete local directory + parent.target?.fileModel.refreshAll(); + } } else if (name == 'job_error') { parent.target?.fileModel.jobController.jobError(evt); } else if (name == 'override_file_confirm') { From f34b8411a795ef16208ab1e2ace7e53147c41d3f Mon Sep 17 00:00:00 2001 From: 21pages Date: Tue, 20 Aug 2024 15:34:10 +0800 Subject: [PATCH 121/541] Fix new cm tab not replace the old persisted tab (#9127) * This happens when after changing DesktopTab to StatefulWidget, 1.2.7 and 1.3.0 have this problem. * When `addConnection` in server_model.dart is called, the old closed client is removed, the client parameter of buildConnectionCard is new, but client id inside Consumer is old. * The only state in cm page is timer, its value is kept in test. * There may be a better way to solve the ui update. Signed-off-by: 21pages --- flutter/lib/desktop/widgets/tabbar_widget.dart | 7 +++++++ flutter/lib/models/server_model.dart | 4 +++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index 0b48b3b92fc3..75ecacbfe580 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -552,6 +552,13 @@ class _DesktopTabState extends State controller: state.value.pageController, physics: NeverScrollableScrollPhysics(), children: () { + if (DesktopTabType.cm == tabType) { + // Fix when adding a new tab still showing closed tabs with the same peer id, which would happen after the DesktopTab was stateful. + return state.value.tabs.map((tab) { + return tab.page; + }).toList(); + } + /// to-do refactor, separate connection state and UI state for remote session. /// [workaround] PageView children need an immutable list, after it has been passed into PageView final tabLen = state.value.tabs.length; diff --git a/flutter/lib/models/server_model.dart b/flutter/lib/models/server_model.dart index 613fee6ad117..1d800ef69678 100644 --- a/flutter/lib/models/server_model.dart +++ b/flutter/lib/models/server_model.dart @@ -826,7 +826,7 @@ class Client { Map toJson() { final Map data = {}; data['id'] = id; - data['is_start'] = authorized; + data['authorized'] = authorized; data['is_file_transfer'] = isFileTransfer; data['port_forward'] = portForward; data['name'] = name; @@ -840,6 +840,8 @@ class Client { data['block_input'] = blockInput; data['disconnected'] = disconnected; data['from_switch'] = fromSwitch; + data['in_voice_call'] = inVoiceCall; + data['incoming_voice_call'] = incomingVoiceCall; return data; } From e3cce2824d19cd74107f69f6f9e7428b93d189f6 Mon Sep 17 00:00:00 2001 From: Daniel Ehrhardt Date: Wed, 21 Aug 2024 03:28:02 +0200 Subject: [PATCH 122/541] Added Public Server to Readme (#9132) --- README.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 8e2d9adb5b91..daf4a590743b 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@

RustDesk - Your remote desktop
-
Servers • + ServersBuildDockerStructure • @@ -171,3 +171,13 @@ Please ensure that you are running these commands from the root of the RustDesk ![File Transfer](https://github.com/rustdesk/rustdesk/assets/28412477/39511ad3-aa9a-4f8c-8947-1cce286a46ad) ![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 + +

+ + Codext GmbH + +

\ No newline at end of file From f300d797e2ddadf728c1cb4bfa0fa72b5c66ec89 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Wed, 21 Aug 2024 18:21:25 +0800 Subject: [PATCH 123/541] Fix/ios query onlines (#9134) * fix: ios query onlines Signed-off-by: fufesou * comments Signed-off-by: fufesou --------- Signed-off-by: fufesou --- src/client.rs | 150 +++++++++++++++++++++++++++++++++++++ src/flutter.rs | 3 +- src/rendezvous_mediator.rs | 143 +---------------------------------- 3 files changed, 152 insertions(+), 144 deletions(-) diff --git a/src/client.rs b/src/client.rs index 665560d62155..b687c8a84c15 100644 --- a/src/client.rs +++ b/src/client.rs @@ -3407,3 +3407,153 @@ async fn hc_connection_( } Ok(()) } + +pub mod peer_online { + use hbb_common::{ + anyhow::bail, + config::{Config, CONNECT_TIMEOUT, READ_TIMEOUT}, + log, + rendezvous_proto::*, + sleep, + socket_client::connect_tcp, + tcp::FramedStream, + ResultType, + }; + use std::time::Instant; + + pub async fn query_online_states, Vec)>(ids: Vec, f: F) { + let test = false; + if test { + sleep(1.5).await; + let mut onlines = ids; + let offlines = onlines.drain((onlines.len() / 2)..).collect(); + f(onlines, offlines) + } else { + let query_begin = Instant::now(); + let query_timeout = std::time::Duration::from_millis(3_000); + loop { + match query_online_states_(&ids, query_timeout).await { + Ok((onlines, offlines)) => { + f(onlines, offlines); + break; + } + Err(e) => { + log::debug!("{}", &e); + } + } + + if query_begin.elapsed() > query_timeout { + log::debug!( + "query onlines timeout {:?} ({:?})", + query_begin.elapsed(), + query_timeout + ); + break; + } + + sleep(1.5).await; + } + } + } + + async fn create_online_stream() -> ResultType { + let (rendezvous_server, _servers, _contained) = + crate::get_rendezvous_server(READ_TIMEOUT).await; + let tmp: Vec<&str> = rendezvous_server.split(":").collect(); + if tmp.len() != 2 { + bail!("Invalid server address: {}", rendezvous_server); + } + let port: u16 = tmp[1].parse()?; + if port == 0 { + bail!("Invalid server address: {}", rendezvous_server); + } + let online_server = format!("{}:{}", tmp[0], port - 1); + connect_tcp(online_server, CONNECT_TIMEOUT).await + } + + async fn query_online_states_( + ids: &Vec, + timeout: std::time::Duration, + ) -> ResultType<(Vec, Vec)> { + let query_begin = Instant::now(); + + let mut msg_out = RendezvousMessage::new(); + msg_out.set_online_request(OnlineRequest { + id: Config::get_id(), + peers: ids.clone(), + ..Default::default() + }); + + loop { + let mut socket = match create_online_stream().await { + Ok(s) => s, + Err(e) => { + log::debug!("Failed to create peers online stream, {e}"); + return Ok((vec![], ids.clone())); + } + }; + // TODO: Use long connections to avoid socket creation + // If we use a Arc>> to hold and reuse the previous socket, + // we may face the following error: + // An established connection was aborted by the software in your host machine. (os error 10053) + if let Err(e) = socket.send(&msg_out).await { + log::debug!("Failed to send peers online states query, {e}"); + return Ok((vec![], ids.clone())); + } + if let Some(msg_in) = + crate::common::get_next_nonkeyexchange_msg(&mut socket, None).await + { + match msg_in.union { + Some(rendezvous_message::Union::OnlineResponse(online_response)) => { + let states = online_response.states; + let mut onlines = Vec::new(); + let mut offlines = Vec::new(); + for i in 0..ids.len() { + // bytes index from left to right + let bit_value = 0x01 << (7 - i % 8); + if (states[i / 8] & bit_value) == bit_value { + onlines.push(ids[i].clone()); + } else { + offlines.push(ids[i].clone()); + } + } + return Ok((onlines, offlines)); + } + _ => { + // ignore + } + } + } else { + // TODO: Make sure socket closed? + bail!("Online stream receives None"); + } + + if query_begin.elapsed() > timeout { + bail!("Try query onlines timeout {:?}", &timeout); + } + + sleep(300.0).await; + } + } + + #[cfg(test)] + mod tests { + use hbb_common::tokio; + + #[tokio::test] + async fn test_query_onlines() { + super::query_online_states( + vec![ + "152183996".to_owned(), + "165782066".to_owned(), + "155323351".to_owned(), + "460952777".to_owned(), + ], + |onlines: Vec, offlines: Vec| { + println!("onlines: {:?}, offlines: {:?}", &onlines, &offlines); + }, + ) + .await; + } + } +} diff --git a/src/flutter.rs b/src/flutter.rs index d408202a96d6..03b5a5750309 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -2093,8 +2093,7 @@ pub(super) mod async_tasks { ids = rx_onlines.recv() => { match ids { Some(_ids) => { - #[cfg(not(any(target_os = "ios")))] - crate::rendezvous_mediator::query_online_states(_ids, handle_query_onlines).await + crate::client::peer_online::query_online_states(_ids, handle_query_onlines).await } None => { break; diff --git a/src/rendezvous_mediator.rs b/src/rendezvous_mediator.rs index 4ae222966e97..bd628ec66ef4 100644 --- a/src/rendezvous_mediator.rs +++ b/src/rendezvous_mediator.rs @@ -12,10 +12,7 @@ use uuid::Uuid; use hbb_common::{ allow_err, anyhow::{self, bail}, - config::{ - self, keys::*, option2bool, Config, CONNECT_TIMEOUT, READ_TIMEOUT, REG_INTERVAL, - RENDEZVOUS_PORT, - }, + config::{self, keys::*, option2bool, Config, CONNECT_TIMEOUT, REG_INTERVAL, RENDEZVOUS_PORT}, futures::future::join_all, log, protobuf::Message as _, @@ -703,123 +700,6 @@ async fn direct_server(server: ServerPtr) { } } -pub async fn query_online_states, Vec)>(ids: Vec, f: F) { - let test = false; - if test { - sleep(1.5).await; - let mut onlines = ids; - let offlines = onlines.drain((onlines.len() / 2)..).collect(); - f(onlines, offlines) - } else { - let query_begin = Instant::now(); - let query_timeout = std::time::Duration::from_millis(3_000); - loop { - if SHOULD_EXIT.load(Ordering::SeqCst) { - break; - } - match query_online_states_(&ids, query_timeout).await { - Ok((onlines, offlines)) => { - f(onlines, offlines); - break; - } - Err(e) => { - log::debug!("{}", &e); - } - } - - if query_begin.elapsed() > query_timeout { - log::debug!( - "query onlines timeout {:?} ({:?})", - query_begin.elapsed(), - query_timeout - ); - break; - } - - sleep(1.5).await; - } - } -} - -async fn create_online_stream() -> ResultType { - let (rendezvous_server, _servers, _contained) = - crate::get_rendezvous_server(READ_TIMEOUT).await; - let tmp: Vec<&str> = rendezvous_server.split(":").collect(); - if tmp.len() != 2 { - bail!("Invalid server address: {}", rendezvous_server); - } - let port: u16 = tmp[1].parse()?; - if port == 0 { - bail!("Invalid server address: {}", rendezvous_server); - } - let online_server = format!("{}:{}", tmp[0], port - 1); - connect_tcp(online_server, CONNECT_TIMEOUT).await -} - -async fn query_online_states_( - ids: &Vec, - timeout: std::time::Duration, -) -> ResultType<(Vec, Vec)> { - let query_begin = Instant::now(); - - let mut msg_out = RendezvousMessage::new(); - msg_out.set_online_request(OnlineRequest { - id: Config::get_id(), - peers: ids.clone(), - ..Default::default() - }); - - loop { - if SHOULD_EXIT.load(Ordering::SeqCst) { - // No need to care about onlines - return Ok((Vec::new(), Vec::new())); - } - - let mut socket = match create_online_stream().await { - Ok(s) => s, - Err(e) => { - log::debug!("Failed to create peers online stream, {e}"); - return Ok((vec![], ids.clone())); - } - }; - if let Err(e) = socket.send(&msg_out).await { - log::debug!("Failed to send peers online states query, {e}"); - return Ok((vec![], ids.clone())); - } - if let Some(msg_in) = crate::common::get_next_nonkeyexchange_msg(&mut socket, None).await { - match msg_in.union { - Some(rendezvous_message::Union::OnlineResponse(online_response)) => { - let states = online_response.states; - let mut onlines = Vec::new(); - let mut offlines = Vec::new(); - for i in 0..ids.len() { - // bytes index from left to right - let bit_value = 0x01 << (7 - i % 8); - if (states[i / 8] & bit_value) == bit_value { - onlines.push(ids[i].clone()); - } else { - offlines.push(ids[i].clone()); - } - } - return Ok((onlines, offlines)); - } - _ => { - // ignore - } - } - } else { - // TODO: Make sure socket closed? - bail!("Online stream receives None"); - } - - if query_begin.elapsed() > timeout { - bail!("Try query onlines timeout {:?}", &timeout); - } - - sleep(300.0).await; - } -} - enum Sink<'a> { Framed(&'a mut FramedSocket, &'a TargetAddr<'a>), Stream(&'a mut FramedStream), @@ -833,24 +713,3 @@ impl Sink<'_> { } } } - -#[cfg(test)] -mod tests { - use hbb_common::tokio; - - #[tokio::test] - async fn test_query_onlines() { - super::query_online_states( - vec![ - "152183996".to_owned(), - "165782066".to_owned(), - "155323351".to_owned(), - "460952777".to_owned(), - ], - |onlines: Vec, offlines: Vec| { - println!("onlines: {:?}, offlines: {:?}", &onlines, &offlines); - }, - ) - .await; - } -} From 529e70910dd80ba2688eb6867890f01bfee0b9c0 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Wed, 21 Aug 2024 18:29:43 +0800 Subject: [PATCH 124/541] build 47 --- flutter/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index 965e7f7bd16d..7aadc1c4115e 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # 1.1.9-1 works for android, but for ios it becomes 1.1.91, need to set it to 1.1.9-a.1 for iOS, will get 1.1.9.1, but iOS store not allow 4 numbers -version: 1.3.0+46 +version: 1.3.0+47 environment: sdk: '^3.1.0' From fc607d678997766c64c01a7a9836430011b65843 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Thu, 22 Aug 2024 09:35:50 +0800 Subject: [PATCH 125/541] fix: privacy mode 2, restore (#9141) Signed-off-by: fufesou --- src/privacy_mode/win_virtual_display.rs | 42 +++++++++++++++++++------ src/server/display_service.rs | 36 ++++++++++++++++++--- 2 files changed, 64 insertions(+), 14 deletions(-) diff --git a/src/privacy_mode/win_virtual_display.rs b/src/privacy_mode/win_virtual_display.rs index de85e7ba8dc2..25997f03671c 100644 --- a/src/privacy_mode/win_virtual_display.rs +++ b/src/privacy_mode/win_virtual_display.rs @@ -34,7 +34,7 @@ const CONFIG_KEY_REG_RECOVERY: &str = "reg_recovery"; struct Display { dm: DEVMODEW, name: [WCHAR; 32], - _primary: bool, + primary: bool, } pub struct PrivacyModeImpl { @@ -135,7 +135,7 @@ impl PrivacyModeImpl { let display = Display { dm, name: dd.DeviceName, - _primary: primary, + primary, }; let ds = virtual_display_manager::get_cur_device_string(); @@ -357,6 +357,35 @@ impl PrivacyModeImpl { } Ok(()) } + + fn restore(&mut self) { + Self::restore_displays(&self.displays); + Self::restore_displays(&self.virtual_displays); + allow_err!(Self::commit_change_display(0)); + self.restore_plug_out_monitor(); + self.displays.clear(); + self.virtual_displays.clear(); + } + + fn restore_displays(displays: &[Display]) { + for display in displays { + unsafe { + let mut dm = display.dm.clone(); + let flags = if display.primary { + CDS_NORESET | CDS_UPDATEREGISTRY | CDS_SET_PRIMARY + } else { + CDS_NORESET | CDS_UPDATEREGISTRY + }; + ChangeDisplaySettingsExW( + display.name.as_ptr(), + &mut dm, + std::ptr::null_mut(), + flags, + std::ptr::null_mut(), + ); + } + } + } } impl PrivacyMode for PrivacyModeImpl { @@ -431,14 +460,9 @@ impl PrivacyMode for PrivacyModeImpl { ) -> ResultType<()> { self.check_off_conn_id(conn_id)?; super::win_input::unhook()?; - let virtual_display_added = self.virtual_displays_added.len() > 0; - if virtual_display_added { - self.restore_plug_out_monitor(); - } + let _tmp_ignore_changed_holder = crate::display_service::temp_ignore_displays_changed(); + self.restore(); restore_reg_connectivity(false); - if !virtual_display_added { - Self::commit_change_display(CDS_RESET)?; - } if self.conn_id != INVALID_PRIVACY_MODE_CONN_ID { if let Some(state) = state { diff --git a/src/server/display_service.rs b/src/server/display_service.rs index 7260b9c7a23b..e099e25a096a 100644 --- a/src/server/display_service.rs +++ b/src/server/display_service.rs @@ -1,4 +1,5 @@ use super::*; +use crate::common::SimpleCallOnReturn; #[cfg(target_os = "linux")] use crate::platform::linux::is_x11; #[cfg(windows)] @@ -7,6 +8,7 @@ use crate::virtual_display_manager; use hbb_common::get_version_number; use hbb_common::protobuf::MessageField; use scrap::Display; +use std::sync::atomic::{AtomicBool, Ordering}; // https://github.com/rustdesk/rustdesk/discussions/6042, avoiding dbus call @@ -29,6 +31,9 @@ lazy_static::lazy_static! { static ref SYNC_DISPLAYS: Arc> = Default::default(); } +// https://github.com/rustdesk/rustdesk/pull/8537 +static TEMP_IGNORE_DISPLAYS_CHANGED: AtomicBool = AtomicBool::new(false); + #[derive(Default)] struct SyncDisplaysInfo { displays: Vec, @@ -39,13 +44,17 @@ impl SyncDisplaysInfo { fn check_changed(&mut self, displays: Vec) { if self.displays.len() != displays.len() { self.displays = displays; - self.is_synced = false; + if !TEMP_IGNORE_DISPLAYS_CHANGED.load(Ordering::Relaxed) { + self.is_synced = false; + } return; } for (i, d) in displays.iter().enumerate() { if d != &self.displays[i] { self.displays = displays; - self.is_synced = false; + if !TEMP_IGNORE_DISPLAYS_CHANGED.load(Ordering::Relaxed) { + self.is_synced = false; + } return; } } @@ -60,6 +69,21 @@ impl SyncDisplaysInfo { } } +pub fn temp_ignore_displays_changed() -> SimpleCallOnReturn { + TEMP_IGNORE_DISPLAYS_CHANGED.store(true, std::sync::atomic::Ordering::Relaxed); + SimpleCallOnReturn { + b: true, + f: Box::new(move || { + // Wait for a while to make sure check_display_changed() is called + // after video service has sending its `SwitchDisplay` message(`try_broadcast_display_changed()`). + std::thread::sleep(Duration::from_millis(1000)); + TEMP_IGNORE_DISPLAYS_CHANGED.store(false, Ordering::Relaxed); + // Trigger the display changed message. + SYNC_DISPLAYS.lock().unwrap().is_synced = false; + }), + } +} + // This function is really useful, though a duplicate check if display changed. // The video server will then send the following messages to the client: // 1. the supported resolutions of the {idx} display @@ -204,9 +228,11 @@ fn get_displays_msg() -> Option { fn run(sp: EmptyExtraFieldService) -> ResultType<()> { while sp.ok() { sp.snapshot(|sps| { - if sps.has_subscribes() { - SYNC_DISPLAYS.lock().unwrap().is_synced = false; - bail!("new subscriber"); + if !TEMP_IGNORE_DISPLAYS_CHANGED.load(Ordering::Relaxed) { + if sps.has_subscribes() { + SYNC_DISPLAYS.lock().unwrap().is_synced = false; + bail!("new subscriber"); + } } Ok(()) })?; From 5931af460e3efa01a4d42955837535a3b55ad325 Mon Sep 17 00:00:00 2001 From: jxdv Date: Thu, 22 Aug 2024 01:36:03 +0000 Subject: [PATCH 126/541] Update trs (#9144) * update sk tr * update cz tr --- src/lang/cs.rs | 6 +++--- src/lang/sk.rs | 24 ++++++++++++------------ 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/lang/cs.rs b/src/lang/cs.rs index 41d94d97846a..8ff3d8069187 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -641,8 +641,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Platform", "Platforma"), ("Days remaining", "Zbývajících dnů"), ("enable-trusted-devices-tip", "Přeskočte 2FA ověření na důvěryhodných zařízeních"), - ("Parent directory", ""), - ("Resume", ""), - ("Invalid file name", ""), + ("Parent directory", "Rodičovský adresář"), + ("Resume", "Pokračovat"), + ("Invalid file name", "Nesprávný název souboru"), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index d1f5467afaa0..51d090328349 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -632,17 +632,17 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About RustDesk", "O RustDesk"), ("Send clipboard keystrokes", "Odoslať stlačenia klávesov zo schránky"), ("network_error_tip", "Skontrolujte svoje sieťové pripojenie a potom kliknite na tlačidlo Opakovať."), - ("Unlock with PIN", ""), - ("Requires at least {} characters", ""), - ("Wrong PIN", ""), - ("Set PIN", ""), - ("Enable trusted devices", ""), - ("Manage trusted devices", ""), - ("Platform", ""), - ("Days remaining", ""), - ("enable-trusted-devices-tip", ""), - ("Parent directory", ""), - ("Resume", ""), - ("Invalid file name", ""), + ("Unlock with PIN", "Odomknutie pomocou PIN kódu"), + ("Requires at least {} characters", "Vyžaduje aspoň {} znakov"), + ("Wrong PIN", "Nesprávny PIN kód"), + ("Set PIN", "Nastavenie PIN kódu"), + ("Enable trusted devices", "Povolenie dôveryhodných zariadení"), + ("Manage trusted devices", "Správa dôveryhodných zariadení"), + ("Platform", "Platforma"), + ("Days remaining", "Zostávajúce dni"), + ("enable-trusted-devices-tip", "Vynechanie overovania 2FA na dôveryhodných zariadeniach"), + ("Parent directory", "Rodičovský adresár"), + ("Resume", "Obnoviť"), + ("Invalid file name", "Nesprávny názov súboru"), ].iter().cloned().collect(); } From 50aa8e12ad8c84d66139d46e3afe9cb3a2ad917a Mon Sep 17 00:00:00 2001 From: 21pages Date: Fri, 23 Aug 2024 10:00:36 +0800 Subject: [PATCH 127/541] desktop file transfer, all columns respond to tap, add right click item border (#9153) When right click selected item, the border is not obvious but can feel some change. Signed-off-by: 21pages --- .../lib/desktop/pages/file_manager_page.dart | 103 +++++++++++------- 1 file changed, 63 insertions(+), 40 deletions(-) diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index 682ffa831e8c..05af83cd38d5 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -930,6 +930,7 @@ class _FileManagerViewState extends State { BuildContext context, ScrollController scrollController) { final fd = controller.directory.value; final entries = fd.entries; + Rx rightClickEntry = Rx(null); return ListSearchActionListener( node: _keyboardNode, @@ -989,6 +990,53 @@ class _FileManagerViewState extends State { ? " " : "${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("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( padding: EdgeInsets.symmetric(vertical: 1), child: Obx(() => Container( @@ -999,6 +1047,12 @@ class _FileManagerViewState extends State { borderRadius: BorderRadius.all( Radius.circular(5.0), ), + border: rightClickEntry.value == entry + ? Border.all( + color: MyTheme.button, + width: 1.0, + ) + : null, ), key: ValueKey(entry.name), height: kDesktopFileTransferRowHeight, @@ -1047,46 +1101,9 @@ class _FileManagerViewState extends State { ]), )), ), - 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("Rename"), - height: CustomPopupMenuTheme.height, - onTap: () { - controller.renameAction(entry, isLocal); - }, - ) - ]; - if (items.isNotEmpty) { - mod_menu.showMenu( - context: context, - position: secondaryPosition, - items: items, - ); - } - }, - onSecondaryTapDown: (details) { - secondaryPosition = RelativeRect.fromLTRB( - details.globalPosition.dx, - details.globalPosition.dy, - details.globalPosition.dx, - details.globalPosition.dy); - }, + onTap: onTap, + onSecondaryTap: onSecondaryTap, + onSecondaryTapDown: onSecondaryTapDown, ), SizedBox( width: 2.0, @@ -1111,6 +1128,9 @@ class _FileManagerViewState extends State { )), ), ), + onTap: onTap, + onSecondaryTap: onSecondaryTap, + onSecondaryTapDown: onSecondaryTapDown, ), // Divider from header. SizedBox( @@ -1133,6 +1153,9 @@ class _FileManagerViewState extends State { : MyTheme.darkGray), ), ), + onTap: onTap, + onSecondaryTap: onSecondaryTap, + onSecondaryTapDown: onSecondaryTapDown, ), ), ], From 9d9741f18efd3ef1361c5b0e0d8e562c5f93acdd Mon Sep 17 00:00:00 2001 From: Kleofass <4000163+Kleofass@users.noreply.github.com> Date: Fri, 23 Aug 2024 16:27:52 +0300 Subject: [PATCH 128/541] Update lv.rs (#9155) --- src/lang/lv.rs | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/lang/lv.rs b/src/lang/lv.rs index df2324def573..bb1090dca9c2 100644 --- a/src/lang/lv.rs +++ b/src/lang/lv.rs @@ -629,20 +629,20 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("enable-bot-desc", "1. Atveriet tērzēšanu ar @BotFather.\n2. Nosūtiet komandu \"/newbot\". Pēc šīs darbības pabeigšanas jūs saņemsit pilnvaru.\n3. Sāciet tērzēšanu ar jaunizveidoto robotprogrammatūru. Lai to aktivizētu, nosūtiet ziņojumu, kas sākas ar slīpsvītru (\"/\"), piemēram, \"/hello\".\n"), ("cancel-2fa-confirm-tip", "Vai tiešām vēlaties atcelt 2FA?"), ("cancel-bot-confirm-tip", "Vai tiešām vēlaties atcelt Telegram robotu?"), - ("About RustDesk", ""), - ("Send clipboard keystrokes", ""), - ("network_error_tip", ""), - ("Unlock with PIN", ""), - ("Requires at least {} characters", ""), - ("Wrong PIN", ""), - ("Set PIN", ""), - ("Enable trusted devices", ""), - ("Manage trusted devices", ""), - ("Platform", ""), - ("Days remaining", ""), - ("enable-trusted-devices-tip", ""), - ("Parent directory", ""), - ("Resume", ""), - ("Invalid file name", ""), + ("About RustDesk", "Par RustDesk"), + ("Send clipboard keystrokes", "Nosūtīt starpliktuves taustiņsitienus"), + ("network_error_tip", "Lūdzu, pārbaudiet tīkla savienojumu un pēc tam noklikšķiniet uz Mēģināt vēlreiz."), + ("Unlock with PIN", "Atbloķēt ar PIN"), + ("Requires at least {} characters", "Nepieciešamas vismaz {} rakstzīmes"), + ("Wrong PIN", "Nepareizs PIN"), + ("Set PIN", "Iestatīt PIN"), + ("Enable trusted devices", "Iespējot uzticamas ierīces"), + ("Manage trusted devices", "Pārvaldīt uzticamas ierīces"), + ("Platform", "Platforma"), + ("Days remaining", "Atlikušas dienas"), + ("enable-trusted-devices-tip", "Izlaist 2FA verifikāciju uzticamās ierīcēs"), + ("Parent directory", "Vecākdirektorijs"), + ("Resume", "Atsākt"), + ("Invalid file name", "Nederīgs faila nosaukums"), ].iter().cloned().collect(); } From 1d416f6626ad02a47b245697b32fb2c374e928f4 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Sat, 24 Aug 2024 12:10:36 +0800 Subject: [PATCH 129/541] refact: flutter keyboard, map mode (#9160) Signed-off-by: fufesou --- Cargo.lock | 2 +- flutter/lib/common/widgets/remote_input.dart | 3 +- .../lib/models/desktop_render_texture.dart | 1 + flutter/lib/models/input_model.dart | 292 ++++++++---------- flutter/lib/web/bridge.dart | 71 ++++- flutter/lib/web/texture_rgba_renderer.dart | 2 +- src/flutter_ffi.rs | 10 +- src/ui_session_interface.rs | 60 ++-- 8 files changed, 231 insertions(+), 210 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ef2cab92c807..1b1e66826c63 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5187,7 +5187,7 @@ dependencies = [ [[package]] name = "rdev" version = "0.5.0-2" -source = "git+https://github.com/rustdesk-org/rdev#b3434caee84c92412b45a2f655a15ac5dad33488" +source = "git+https://github.com/rustdesk-org/rdev#d4c1759926d693ba269e2cb8cf9f87b13e424e4e" dependencies = [ "cocoa 0.24.1", "core-foundation 0.9.4", diff --git a/flutter/lib/common/widgets/remote_input.dart b/flutter/lib/common/widgets/remote_input.dart index 61bd4dd31bc5..73e5d8f39a83 100644 --- a/flutter/lib/common/widgets/remote_input.dart +++ b/flutter/lib/common/widgets/remote_input.dart @@ -34,8 +34,7 @@ class RawKeyFocusScope extends StatelessWidget { canRequestFocus: true, focusNode: focusNode, onFocusChange: onFocusChange, - onKey: (FocusNode data, RawKeyEvent e) => - inputModel.handleRawKeyEvent(e), + onKeyEvent: (node, event) => inputModel.handleKeyEvent(event), child: child)); } } diff --git a/flutter/lib/models/desktop_render_texture.dart b/flutter/lib/models/desktop_render_texture.dart index ab8df3c4539e..c6cf55256de8 100644 --- a/flutter/lib/models/desktop_render_texture.dart +++ b/flutter/lib/models/desktop_render_texture.dart @@ -181,6 +181,7 @@ class TextureModel { } updateCurrentDisplay(int curDisplay) { + if (isWeb) return; final ffi = parent.target; if (ffi == null) return; tryCreateTexture(int idx) { diff --git a/flutter/lib/models/input_model.dart b/flutter/lib/models/input_model.dart index dde815789ae2..35f00c3e1c37 100644 --- a/flutter/lib/models/input_model.dart +++ b/flutter/lib/models/input_model.dart @@ -178,15 +178,15 @@ class PointerEventToRust { } class ToReleaseKeys { - RawKeyEvent? lastLShiftKeyEvent; - RawKeyEvent? lastRShiftKeyEvent; - RawKeyEvent? lastLCtrlKeyEvent; - RawKeyEvent? lastRCtrlKeyEvent; - RawKeyEvent? lastLAltKeyEvent; - RawKeyEvent? lastRAltKeyEvent; - RawKeyEvent? lastLCommandKeyEvent; - RawKeyEvent? lastRCommandKeyEvent; - RawKeyEvent? lastSuperKeyEvent; + KeyEvent? lastLShiftKeyEvent; + KeyEvent? lastRShiftKeyEvent; + KeyEvent? lastLCtrlKeyEvent; + KeyEvent? lastRCtrlKeyEvent; + KeyEvent? lastLAltKeyEvent; + KeyEvent? lastRAltKeyEvent; + KeyEvent? lastLCommandKeyEvent; + KeyEvent? lastRCommandKeyEvent; + KeyEvent? lastSuperKeyEvent; reset() { lastLShiftKeyEvent = null; @@ -200,67 +200,7 @@ class ToReleaseKeys { lastSuperKeyEvent = null; } - updateKeyDown(LogicalKeyboardKey logicKey, RawKeyDownEvent e) { - if (e.isAltPressed) { - if (logicKey == LogicalKeyboardKey.altLeft) { - lastLAltKeyEvent = e; - } else if (logicKey == LogicalKeyboardKey.altRight) { - lastRAltKeyEvent = e; - } - } else if (e.isControlPressed) { - if (logicKey == LogicalKeyboardKey.controlLeft) { - lastLCtrlKeyEvent = e; - } else if (logicKey == LogicalKeyboardKey.controlRight) { - lastRCtrlKeyEvent = e; - } - } else if (e.isShiftPressed) { - if (logicKey == LogicalKeyboardKey.shiftLeft) { - lastLShiftKeyEvent = e; - } else if (logicKey == LogicalKeyboardKey.shiftRight) { - lastRShiftKeyEvent = e; - } - } else if (e.isMetaPressed) { - if (logicKey == LogicalKeyboardKey.metaLeft) { - lastLCommandKeyEvent = e; - } else if (logicKey == LogicalKeyboardKey.metaRight) { - lastRCommandKeyEvent = e; - } else if (logicKey == LogicalKeyboardKey.superKey) { - lastSuperKeyEvent = e; - } - } - } - - updateKeyUp(LogicalKeyboardKey logicKey, RawKeyUpEvent e) { - if (e.isAltPressed) { - if (logicKey == LogicalKeyboardKey.altLeft) { - lastLAltKeyEvent = null; - } else if (logicKey == LogicalKeyboardKey.altRight) { - lastRAltKeyEvent = null; - } - } else if (e.isControlPressed) { - if (logicKey == LogicalKeyboardKey.controlLeft) { - lastLCtrlKeyEvent = null; - } else if (logicKey == LogicalKeyboardKey.controlRight) { - lastRCtrlKeyEvent = null; - } - } else if (e.isShiftPressed) { - if (logicKey == LogicalKeyboardKey.shiftLeft) { - lastLShiftKeyEvent = null; - } else if (logicKey == LogicalKeyboardKey.shiftRight) { - lastRShiftKeyEvent = null; - } - } else if (e.isMetaPressed) { - if (logicKey == LogicalKeyboardKey.metaLeft) { - lastLCommandKeyEvent = null; - } else if (logicKey == LogicalKeyboardKey.metaRight) { - lastRCommandKeyEvent = null; - } else if (logicKey == LogicalKeyboardKey.superKey) { - lastSuperKeyEvent = null; - } - } - } - - release(KeyEventResult Function(RawKeyEvent e) handleRawKeyEvent) { + release(KeyEventResult Function(KeyEvent e) handleKeyEvent) { for (final key in [ lastLShiftKeyEvent, lastRShiftKeyEvent, @@ -273,10 +213,7 @@ class ToReleaseKeys { lastSuperKeyEvent, ]) { if (key != null) { - handleRawKeyEvent(RawKeyUpEvent( - data: key.data, - character: key.character, - )); + handleKeyEvent(key); } } } @@ -339,49 +276,116 @@ class InputModel { } } - KeyEventResult handleRawKeyEvent(RawKeyEvent e) { + void handleKeyDownEventModifiers(KeyEvent e) { + KeyUpEvent upEvent(e) => KeyUpEvent( + physicalKey: e.physicalKey, + logicalKey: e.logicalKey, + timeStamp: e.timeStamp, + ); + if (e.logicalKey == LogicalKeyboardKey.altLeft) { + if (!alt) { + alt = true; + } + toReleaseKeys.lastLAltKeyEvent = upEvent(e); + } else if (e.logicalKey == LogicalKeyboardKey.altRight) { + if (!alt) { + alt = true; + } + toReleaseKeys.lastLAltKeyEvent = upEvent(e); + } else if (e.logicalKey == LogicalKeyboardKey.controlLeft) { + if (!ctrl) { + ctrl = true; + } + toReleaseKeys.lastLCtrlKeyEvent = upEvent(e); + } else if (e.logicalKey == LogicalKeyboardKey.controlRight) { + if (!ctrl) { + ctrl = true; + } + toReleaseKeys.lastRCtrlKeyEvent = upEvent(e); + } else if (e.logicalKey == LogicalKeyboardKey.shiftLeft) { + if (!shift) { + shift = true; + } + toReleaseKeys.lastLShiftKeyEvent = upEvent(e); + } else if (e.logicalKey == LogicalKeyboardKey.shiftRight) { + if (!shift) { + shift = true; + } + toReleaseKeys.lastRShiftKeyEvent = upEvent(e); + } else if (e.logicalKey == LogicalKeyboardKey.metaLeft) { + if (!command) { + command = true; + } + toReleaseKeys.lastLCommandKeyEvent = upEvent(e); + } else if (e.logicalKey == LogicalKeyboardKey.metaRight) { + if (!command) { + command = true; + } + toReleaseKeys.lastRCommandKeyEvent = upEvent(e); + } else if (e.logicalKey == LogicalKeyboardKey.superKey) { + if (!command) { + command = true; + } + toReleaseKeys.lastSuperKeyEvent = upEvent(e); + } + } + + void handleKeyUpEventModifiers(KeyEvent e) { + if (e.logicalKey == LogicalKeyboardKey.altLeft) { + alt = false; + toReleaseKeys.lastLAltKeyEvent = null; + } else if (e.logicalKey == LogicalKeyboardKey.altRight) { + alt = false; + toReleaseKeys.lastRAltKeyEvent = null; + } else if (e.logicalKey == LogicalKeyboardKey.controlLeft) { + ctrl = false; + toReleaseKeys.lastLCtrlKeyEvent = null; + } else if (e.logicalKey == LogicalKeyboardKey.controlRight) { + ctrl = false; + toReleaseKeys.lastRCtrlKeyEvent = null; + } else if (e.logicalKey == LogicalKeyboardKey.shiftLeft) { + shift = false; + toReleaseKeys.lastLShiftKeyEvent = null; + } else if (e.logicalKey == LogicalKeyboardKey.shiftRight) { + shift = false; + toReleaseKeys.lastRShiftKeyEvent = null; + } else if (e.logicalKey == LogicalKeyboardKey.metaLeft) { + command = false; + toReleaseKeys.lastLCommandKeyEvent = null; + } else if (e.logicalKey == LogicalKeyboardKey.metaRight) { + command = false; + toReleaseKeys.lastRCommandKeyEvent = null; + } else if (e.logicalKey == LogicalKeyboardKey.superKey) { + command = false; + toReleaseKeys.lastSuperKeyEvent = null; + } + } + + KeyEventResult handleKeyEvent(KeyEvent e) { if (isViewOnly) return KeyEventResult.handled; if ((isDesktop || isWebDesktop) && !isInputSourceFlutter) { return KeyEventResult.handled; } - - final key = e.logicalKey; - if (e is RawKeyDownEvent) { - if (!e.repeat) { - if (e.isAltPressed && !alt) { - alt = true; - } else if (e.isControlPressed && !ctrl) { - ctrl = true; - } else if (e.isShiftPressed && !shift) { - shift = true; - } else if (e.isMetaPressed && !command) { - command = true; - } + if (isWindows || isLinux) { + // Ignore meta keys. Because flutter window will loose focus if meta key is pressed. + if (e.physicalKey == PhysicalKeyboardKey.metaLeft || + e.physicalKey == PhysicalKeyboardKey.metaRight) { + return KeyEventResult.handled; } - toReleaseKeys.updateKeyDown(key, e); } - if (e is RawKeyUpEvent) { - if (key == LogicalKeyboardKey.altLeft || - key == LogicalKeyboardKey.altRight) { - alt = false; - } else if (key == LogicalKeyboardKey.controlLeft || - key == LogicalKeyboardKey.controlRight) { - ctrl = false; - } else if (key == LogicalKeyboardKey.shiftRight || - key == LogicalKeyboardKey.shiftLeft) { - shift = false; - } else if (key == LogicalKeyboardKey.metaLeft || - key == LogicalKeyboardKey.metaRight || - key == LogicalKeyboardKey.superKey) { - command = false; - } - toReleaseKeys.updateKeyUp(key, e); + if (e is KeyUpEvent) { + handleKeyUpEventModifiers(e); + } else if (e is KeyDownEvent) { + handleKeyDownEventModifiers(e); } // * Currently mobile does not enable map mode - if ((isDesktop || isWebDesktop) && keyboardMode == 'map') { - mapKeyboardMode(e); + if ((isDesktop || isWebDesktop)) { + // FIXME: e.character is wrong for dead keys, eg: ^ in de + newKeyboardMode(e.character ?? '', e.physicalKey.usbHidUsage & 0xFFFF, + // Show repeat event be converted to "release+press" events? + e is KeyDownEvent || e is KeyRepeatEvent); } else { legacyKeyboardMode(e); } @@ -389,42 +393,8 @@ class InputModel { return KeyEventResult.handled; } - void mapKeyboardMode(RawKeyEvent e) { - int positionCode = -1; - int platformCode = -1; - bool down; - - if (e.data is RawKeyEventDataMacOs) { - RawKeyEventDataMacOs newData = e.data as RawKeyEventDataMacOs; - positionCode = newData.keyCode; - platformCode = newData.keyCode; - } else if (e.data is RawKeyEventDataWindows) { - RawKeyEventDataWindows newData = e.data as RawKeyEventDataWindows; - positionCode = newData.scanCode; - platformCode = newData.keyCode; - } else if (e.data is RawKeyEventDataLinux) { - RawKeyEventDataLinux newData = e.data as RawKeyEventDataLinux; - // scanCode and keyCode of RawKeyEventDataLinux are incorrect. - // 1. scanCode means keycode - // 2. keyCode means keysym - positionCode = newData.scanCode; - platformCode = newData.keyCode; - } else if (e.data is RawKeyEventDataAndroid) { - RawKeyEventDataAndroid newData = e.data as RawKeyEventDataAndroid; - positionCode = newData.scanCode + 8; - platformCode = newData.keyCode; - } else {} - - if (e is RawKeyDownEvent) { - down = true; - } else { - down = false; - } - inputRawKey(e.character ?? '', platformCode, positionCode, down); - } - - /// Send raw Key Event - void inputRawKey(String name, int platformCode, int positionCode, bool down) { + /// Send Key Event + void newKeyboardMode(String character, int usbHid, bool down) { const capslock = 1; const numlock = 2; const scrolllock = 3; @@ -443,27 +413,23 @@ class InputModel { } bind.sessionHandleFlutterKeyEvent( sessionId: sessionId, - name: name, - platformCode: platformCode, - positionCode: positionCode, + character: character, + usbHid: usbHid, lockModes: lockModes, downOrUp: down); } - void legacyKeyboardMode(RawKeyEvent e) { - if (e is RawKeyDownEvent) { - if (e.repeat) { - sendRawKey(e, press: true); - } else { - sendRawKey(e, down: true); - } - } - if (e is RawKeyUpEvent) { - sendRawKey(e); + void legacyKeyboardMode(KeyEvent e) { + if (e is KeyDownEvent) { + sendKey(e, down: true); + } else if (e is KeyRepeatEvent) { + sendKey(e, press: true); + } else if (e is KeyUpEvent) { + sendKey(e); } } - void sendRawKey(RawKeyEvent e, {bool? down, bool? press}) { + void sendKey(KeyEvent e, {bool? down, bool? press}) { // for maximum compatibility final label = physicalKeyMap[e.physicalKey.usbHidUsage] ?? logicalKeyMap[e.logicalKey.keyId] ?? @@ -566,7 +532,7 @@ class InputModel { } void enterOrLeave(bool enter) { - toReleaseKeys.release(handleRawKeyEvent); + toReleaseKeys.release(handleKeyEvent); _pointerMovedAfterEnter = false; // Fix status @@ -1164,15 +1130,15 @@ class InputModel { // Simulate a key press event. // `usbHidUsage` is the USB HID usage code of the key. Future tapHidKey(int usbHidUsage) async { - inputRawKey(kKeyFlutterKey, usbHidUsage, 0, true); + newKeyboardMode(kKeyFlutterKey, usbHidUsage, true); await Future.delayed(Duration(milliseconds: 100)); - inputRawKey(kKeyFlutterKey, usbHidUsage, 0, false); + newKeyboardMode(kKeyFlutterKey, usbHidUsage, false); } Future onMobileVolumeUp() async => - await tapHidKey(PhysicalKeyboardKey.audioVolumeUp.usbHidUsage); + await tapHidKey(PhysicalKeyboardKey.audioVolumeUp.usbHidUsage & 0xFFFF); Future onMobileVolumeDown() async => - await tapHidKey(PhysicalKeyboardKey.audioVolumeDown.usbHidUsage); + await tapHidKey(PhysicalKeyboardKey.audioVolumeDown.usbHidUsage & 0xFFFF); Future onMobilePower() async => - await tapHidKey(PhysicalKeyboardKey.power.usbHidUsage); + await tapHidKey(PhysicalKeyboardKey.power.usbHidUsage & 0xFFFF); } diff --git a/flutter/lib/web/bridge.dart b/flutter/lib/web/bridge.dart index 457911458ce1..5f6f5adef876 100644 --- a/flutter/lib/web/bridge.dart +++ b/flutter/lib/web/bridge.dart @@ -23,6 +23,7 @@ sealed class EventToUI { ) = EventToUI_Rgba; const factory EventToUI.texture( int field0, + bool field1, ) = EventToUI_Texture; } @@ -33,15 +34,19 @@ class EventToUI_Event implements EventToUI { } class EventToUI_Rgba implements EventToUI { - const EventToUI_Rgba(final int field0) : this.field = field0; + const EventToUI_Rgba(final int field0) : field = field0; final int field; int get field0 => field; } class EventToUI_Texture implements EventToUI { - const EventToUI_Texture(final int field0) : this.field = field0; - final int field; - int get field0 => field; + const EventToUI_Texture(final int field0, final bool field1) + : f0 = field0, + f1 = field1; + final int f0; + final bool f1; + int get field0 => f0; + bool get field1 => f1; } class RustdeskImpl { @@ -394,14 +399,20 @@ class RustdeskImpl { Future sessionHandleFlutterKeyEvent( {required UuidValue sessionId, - required String name, - required int platformCode, - required int positionCode, + required String character, + required int usbHid, required int lockModes, required bool downOrUp, dynamic hint}) { - // TODO: map mode - throw UnimplementedError(); + return Future(() => js.context.callMethod('setByName', [ + 'flutter_key_event', + jsonEncode({ + 'name': character, + 'usb_hid': usbHid, + 'lock_modes': lockModes, + if (downOrUp) 'down': 'true', + }) + ])); } void sessionEnterOrLeave( @@ -702,11 +713,11 @@ class RustdeskImpl { } Future mainGetAppName({dynamic hint}) { - throw UnimplementedError(); + return Future.value(mainGetAppNameSync(hint: hint)); } String mainGetAppNameSync({dynamic hint}) { - throw UnimplementedError(); + return 'RustDesk'; } String mainUriPrefixSync({dynamic hint}) { @@ -758,8 +769,9 @@ class RustdeskImpl { } Future mainIsUsingPublicServer({dynamic hint}) { - return Future( - () => js.context.callMethod('setByName', ["is_using_public_server"])); + return Future(() => + js.context.callMethod('getByName', ["is_using_public_server"]) == + 'true'); } Future mainDiscover({dynamic hint}) { @@ -1610,7 +1622,7 @@ class RustdeskImpl { } bool mainIsOptionFixed({required String key, dynamic hint}) { - throw UnimplementedError(); + return false; } bool mainGetUseTextureRender({dynamic hint}) { @@ -1650,5 +1662,36 @@ class RustdeskImpl { throw UnimplementedError(); } + Future getVoiceCallInputDevice({required bool isCm, dynamic hint}) { + throw UnimplementedError(); + } + + Future setVoiceCallInputDevice( + {required bool isCm, required String device, dynamic hint}) { + throw UnimplementedError(); + } + + bool isPresetPasswordMobileOnly({dynamic hint}) { + throw UnimplementedError(); + } + + String mainGetBuildinOption({required String key, dynamic hint}) { + return ''; + } + + String installInstallOptions({dynamic hint}) { + throw UnimplementedError(); + } + + sessionRenameFile( + {required UuidValue sessionId, + required int actId, + required String path, + required String newName, + required bool isRemote, + dynamic hint}) { + throw UnimplementedError(); + } + void dispose() {} } diff --git a/flutter/lib/web/texture_rgba_renderer.dart b/flutter/lib/web/texture_rgba_renderer.dart index 83407773583a..9a4a1879b12d 100644 --- a/flutter/lib/web/texture_rgba_renderer.dart +++ b/flutter/lib/web/texture_rgba_renderer.dart @@ -6,7 +6,7 @@ class TextureRgbaRenderer { } Future closeTexture(int key) { - throw UnimplementedError(); + return Future(() => true); } Future onRgba( diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 9b9914cfd0fb..e310febb150e 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -491,9 +491,8 @@ pub fn session_switch_display(is_desktop: bool, session_id: SessionID, value: Ve pub fn session_handle_flutter_key_event( session_id: SessionID, - name: String, - platform_code: i32, - position_code: i32, + character: String, + usb_hid: i32, lock_modes: i32, down_or_up: bool, ) { @@ -501,9 +500,8 @@ pub fn session_handle_flutter_key_event( let keyboard_mode = session.get_keyboard_mode(); session.handle_flutter_key_event( &keyboard_mode, - &name, - platform_code, - position_code, + &character, + usb_hid, lock_modes, down_or_up, ); diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 0e030aa8ba47..423794be58ee 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -803,19 +803,18 @@ impl Session { pub fn handle_flutter_key_event( &self, keyboard_mode: &str, - name: &str, - platform_code: i32, - position_code: i32, + character: &str, + usb_hid: i32, lock_modes: i32, down_or_up: bool, ) { - if name == "flutter_key" { - self._handle_key_flutter_simulation(keyboard_mode, platform_code, down_or_up); + if character == "flutter_key" { + self._handle_key_flutter_simulation(keyboard_mode, usb_hid, down_or_up); } else { self._handle_key_non_flutter_simulation( keyboard_mode, - platform_code, - position_code, + character, + usb_hid, lock_modes, down_or_up, ); @@ -831,10 +830,10 @@ impl Session { ) { // https://github.com/flutter/flutter/blob/master/packages/flutter/lib/src/services/keyboard_key.g.dart#L4356 let ctrl_key = match platform_code { - 0x0007007f => Some(ControlKey::VolumeMute), - 0x00070080 => Some(ControlKey::VolumeUp), - 0x00070081 => Some(ControlKey::VolumeDown), - 0x00070066 => Some(ControlKey::Power), + 0x007f => Some(ControlKey::VolumeMute), + 0x0080 => Some(ControlKey::VolumeUp), + 0x0081 => Some(ControlKey::VolumeDown), + 0x0066 => Some(ControlKey::Power), _ => None, }; let Some(ctrl_key) = ctrl_key else { return }; @@ -851,22 +850,28 @@ impl Session { fn _handle_key_non_flutter_simulation( &self, keyboard_mode: &str, - platform_code: i32, - position_code: i32, + character: &str, + usb_hid: i32, lock_modes: i32, down_or_up: bool, ) { - if position_code < 0 || platform_code < 0 { - return; - } - let platform_code: u32 = platform_code as _; - let position_code: KeyCode = position_code as _; + let key = rdev::usb_hid_key_from_code(usb_hid as _); - #[cfg(not(target_os = "windows"))] - let key = rdev::key_from_code(position_code) as rdev::Key; - // Windows requires special handling #[cfg(target_os = "windows")] - let key = rdev::get_win_key(platform_code, position_code); + let platform_code: u32 = rdev::win_code_from_key(key).unwrap_or(0); + #[cfg(target_os = "windows")] + let position_code: KeyCode = rdev::win_scancode_from_key(key).unwrap_or(0) as _; + + #[cfg(not(target_os = "windows"))] + let position_code: KeyCode = rdev::code_from_key(key).unwrap_or(0) as _; + #[cfg(not(any(target_os = "windows", target_os = "linux")))] + let platform_code: u32 = position_code as _; + // For translate mode. + // We need to set the platform code (keysym) if is AltGr. + // https://github.com/rustdesk/rustdesk/blob/07cf1b4db5ef2f925efd3b16b87c33ce03c94809/src/keyboard.rs#L1029 + // https://github.com/flutter/flutter/issues/153811 + #[cfg(target_os = "linux")] + let platform_code: u32 = position_code as _; let event_type = if down_or_up { KeyPress(key) @@ -875,7 +880,16 @@ impl Session { }; let event = Event { time: SystemTime::now(), - unicode: None, + unicode: if character.is_empty() { + None + } else { + Some(rdev::UnicodeInfo { + name: Some(character.to_string()), + unicode: character.encode_utf16().collect(), + // is_dead: is not correct here, because flutter cannot detect deadcode for now. + is_dead: false, + }) + }, platform_code, position_code: position_code as _, event_type, From d400999b9c5f3ce5dadc363a00199702c8d963fa Mon Sep 17 00:00:00 2001 From: rustdesk Date: Sat, 24 Aug 2024 19:02:04 +0800 Subject: [PATCH 130/541] bump to 1.3.1 --- .github/workflows/flutter-build.yml | 2 +- .github/workflows/playground.yml | 2 +- Cargo.lock | 4 ++-- Cargo.toml | 2 +- appimage/AppImageBuilder-aarch64.yml | 2 +- appimage/AppImageBuilder-x86_64.yml | 2 +- flutter/pubspec.yaml | 2 +- libs/portable/Cargo.toml | 2 +- res/PKGBUILD | 2 +- res/rpm-flutter-suse.spec | 2 +- res/rpm-flutter.spec | 2 +- res/rpm.spec | 2 +- 12 files changed, 13 insertions(+), 13 deletions(-) diff --git a/.github/workflows/flutter-build.yml b/.github/workflows/flutter-build.yml index 74ebe72a4b63..aea6127478cc 100644 --- a/.github/workflows/flutter-build.yml +++ b/.github/workflows/flutter-build.yml @@ -33,7 +33,7 @@ env: VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite" # vcpkg version: 2024.07.12 VCPKG_COMMIT_ID: "1de2026f28ead93ff1773e6e680387643e914ea1" - VERSION: "1.3.0" + VERSION: "1.3.1" NDK_VERSION: "r27" #signing keys env variable checks ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}" diff --git a/.github/workflows/playground.yml b/.github/workflows/playground.yml index 3fdcc4cfee85..fb7b89614506 100644 --- a/.github/workflows/playground.yml +++ b/.github/workflows/playground.yml @@ -18,7 +18,7 @@ env: VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite" # vcpkg version: 2024.06.15 VCPKG_COMMIT_ID: "f7423ee180c4b7f40d43402c2feb3859161ef625" - VERSION: "1.3.0" + VERSION: "1.3.1" NDK_VERSION: "r26d" #signing keys env variable checks ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}" diff --git a/Cargo.lock b/Cargo.lock index 1b1e66826c63..fbf1f8ebb9c0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5462,7 +5462,7 @@ dependencies = [ [[package]] name = "rustdesk" -version = "1.3.0" +version = "1.3.1" dependencies = [ "android-wakelock", "android_logger", @@ -5559,7 +5559,7 @@ dependencies = [ [[package]] name = "rustdesk-portable-packer" -version = "1.3.0" +version = "1.3.1" dependencies = [ "brotli", "dirs 5.0.1", diff --git a/Cargo.toml b/Cargo.toml index 5a052af0dfd0..28c2c363d46e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rustdesk" -version = "1.3.0" +version = "1.3.1" authors = ["rustdesk "] edition = "2021" build= "build.rs" diff --git a/appimage/AppImageBuilder-aarch64.yml b/appimage/AppImageBuilder-aarch64.yml index c64966f28192..e23cde7172df 100644 --- a/appimage/AppImageBuilder-aarch64.yml +++ b/appimage/AppImageBuilder-aarch64.yml @@ -18,7 +18,7 @@ AppDir: id: rustdesk name: rustdesk icon: rustdesk - version: 1.3.0 + version: 1.3.1 exec: usr/lib/rustdesk/rustdesk exec_args: $@ apt: diff --git a/appimage/AppImageBuilder-x86_64.yml b/appimage/AppImageBuilder-x86_64.yml index e1b460364b34..83df32ec87bc 100644 --- a/appimage/AppImageBuilder-x86_64.yml +++ b/appimage/AppImageBuilder-x86_64.yml @@ -18,7 +18,7 @@ AppDir: id: rustdesk name: rustdesk icon: rustdesk - version: 1.3.0 + version: 1.3.1 exec: usr/lib/rustdesk/rustdesk exec_args: $@ apt: diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index 7aadc1c4115e..56022b6ec3f7 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # 1.1.9-1 works for android, but for ios it becomes 1.1.91, need to set it to 1.1.9-a.1 for iOS, will get 1.1.9.1, but iOS store not allow 4 numbers -version: 1.3.0+47 +version: 1.3.1+47 environment: sdk: '^3.1.0' diff --git a/libs/portable/Cargo.toml b/libs/portable/Cargo.toml index ce1c10c09e40..10d16605b2b3 100644 --- a/libs/portable/Cargo.toml +++ b/libs/portable/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rustdesk-portable-packer" -version = "1.3.0" +version = "1.3.1" edition = "2021" description = "RustDesk Remote Desktop" diff --git a/res/PKGBUILD b/res/PKGBUILD index 94ccce6937d0..a805997cd888 100644 --- a/res/PKGBUILD +++ b/res/PKGBUILD @@ -1,5 +1,5 @@ pkgname=rustdesk -pkgver=1.3.0 +pkgver=1.3.1 pkgrel=0 epoch= pkgdesc="" diff --git a/res/rpm-flutter-suse.spec b/res/rpm-flutter-suse.spec index 053099c07fa2..2995c54d4354 100644 --- a/res/rpm-flutter-suse.spec +++ b/res/rpm-flutter-suse.spec @@ -1,5 +1,5 @@ Name: rustdesk -Version: 1.3.0 +Version: 1.3.1 Release: 0 Summary: RPM package License: GPL-3.0 diff --git a/res/rpm-flutter.spec b/res/rpm-flutter.spec index f962a2ed1f6a..48a7e2ac0001 100644 --- a/res/rpm-flutter.spec +++ b/res/rpm-flutter.spec @@ -1,5 +1,5 @@ Name: rustdesk -Version: 1.3.0 +Version: 1.3.1 Release: 0 Summary: RPM package License: GPL-3.0 diff --git a/res/rpm.spec b/res/rpm.spec index 633c2a220a78..8c99f9bb0b41 100644 --- a/res/rpm.spec +++ b/res/rpm.spec @@ -1,5 +1,5 @@ Name: rustdesk -Version: 1.3.0 +Version: 1.3.1 Release: 0 Summary: RPM package License: GPL-3.0 From aa1e1225320e50ceb8227a04a880afa06e113b6b Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Sun, 25 Aug 2024 00:03:31 +0800 Subject: [PATCH 131/541] fix: revert key events to raw key events on Linux (#9161) Signed-off-by: fufesou --- flutter/lib/common/widgets/remote_input.dart | 13 +- flutter/lib/models/input_model.dart | 245 ++++++++++++++++++- src/flutter_ffi.rs | 21 ++ src/ui_session_interface.rs | 76 +++++- 4 files changed, 350 insertions(+), 5 deletions(-) diff --git a/flutter/lib/common/widgets/remote_input.dart b/flutter/lib/common/widgets/remote_input.dart index 73e5d8f39a83..fb19c4c23456 100644 --- a/flutter/lib/common/widgets/remote_input.dart +++ b/flutter/lib/common/widgets/remote_input.dart @@ -27,6 +27,10 @@ class RawKeyFocusScope extends StatelessWidget { @override 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( autofocus: true, child: Focus( @@ -34,7 +38,14 @@ class RawKeyFocusScope extends StatelessWidget { canRequestFocus: true, focusNode: focusNode, onFocusChange: onFocusChange, - onKeyEvent: (node, event) => inputModel.handleKeyEvent(event), + onKey: useRawKeyEvents + ? (FocusNode data, RawKeyEvent event) => + inputModel.handleRawKeyEvent(event) + : null, + onKeyEvent: useRawKeyEvents + ? null + : (FocusNode node, KeyEvent event) => + inputModel.handleKeyEvent(event), child: child)); } } diff --git a/flutter/lib/models/input_model.dart b/flutter/lib/models/input_model.dart index 35f00c3e1c37..54e7a3f4dc04 100644 --- a/flutter/lib/models/input_model.dart +++ b/flutter/lib/models/input_model.dart @@ -177,6 +177,111 @@ class PointerEventToRust { } } +class ToReleaseRawKeys { + RawKeyEvent? lastLShiftKeyEvent; + RawKeyEvent? lastRShiftKeyEvent; + RawKeyEvent? lastLCtrlKeyEvent; + RawKeyEvent? lastRCtrlKeyEvent; + RawKeyEvent? lastLAltKeyEvent; + RawKeyEvent? lastRAltKeyEvent; + RawKeyEvent? lastLCommandKeyEvent; + RawKeyEvent? lastRCommandKeyEvent; + RawKeyEvent? lastSuperKeyEvent; + + reset() { + lastLShiftKeyEvent = null; + lastRShiftKeyEvent = null; + lastLCtrlKeyEvent = null; + lastRCtrlKeyEvent = null; + lastLAltKeyEvent = null; + lastRAltKeyEvent = null; + lastLCommandKeyEvent = null; + lastRCommandKeyEvent = null; + lastSuperKeyEvent = null; + } + + updateKeyDown(LogicalKeyboardKey logicKey, RawKeyDownEvent e) { + if (e.isAltPressed) { + if (logicKey == LogicalKeyboardKey.altLeft) { + lastLAltKeyEvent = e; + } else if (logicKey == LogicalKeyboardKey.altRight) { + lastRAltKeyEvent = e; + } + } else if (e.isControlPressed) { + if (logicKey == LogicalKeyboardKey.controlLeft) { + lastLCtrlKeyEvent = e; + } else if (logicKey == LogicalKeyboardKey.controlRight) { + lastRCtrlKeyEvent = e; + } + } else if (e.isShiftPressed) { + if (logicKey == LogicalKeyboardKey.shiftLeft) { + lastLShiftKeyEvent = e; + } else if (logicKey == LogicalKeyboardKey.shiftRight) { + lastRShiftKeyEvent = e; + } + } else if (e.isMetaPressed) { + if (logicKey == LogicalKeyboardKey.metaLeft) { + lastLCommandKeyEvent = e; + } else if (logicKey == LogicalKeyboardKey.metaRight) { + lastRCommandKeyEvent = e; + } else if (logicKey == LogicalKeyboardKey.superKey) { + lastSuperKeyEvent = e; + } + } + } + + updateKeyUp(LogicalKeyboardKey logicKey, RawKeyUpEvent e) { + if (e.isAltPressed) { + if (logicKey == LogicalKeyboardKey.altLeft) { + lastLAltKeyEvent = null; + } else if (logicKey == LogicalKeyboardKey.altRight) { + lastRAltKeyEvent = null; + } + } else if (e.isControlPressed) { + if (logicKey == LogicalKeyboardKey.controlLeft) { + lastLCtrlKeyEvent = null; + } else if (logicKey == LogicalKeyboardKey.controlRight) { + lastRCtrlKeyEvent = null; + } + } else if (e.isShiftPressed) { + if (logicKey == LogicalKeyboardKey.shiftLeft) { + lastLShiftKeyEvent = null; + } else if (logicKey == LogicalKeyboardKey.shiftRight) { + lastRShiftKeyEvent = null; + } + } else if (e.isMetaPressed) { + if (logicKey == LogicalKeyboardKey.metaLeft) { + lastLCommandKeyEvent = null; + } else if (logicKey == LogicalKeyboardKey.metaRight) { + lastRCommandKeyEvent = null; + } else if (logicKey == LogicalKeyboardKey.superKey) { + lastSuperKeyEvent = null; + } + } + } + + release(KeyEventResult Function(RawKeyEvent e) handleRawKeyEvent) { + for (final key in [ + lastLShiftKeyEvent, + lastRShiftKeyEvent, + lastLCtrlKeyEvent, + lastRCtrlKeyEvent, + lastLAltKeyEvent, + lastRAltKeyEvent, + lastLCommandKeyEvent, + lastRCommandKeyEvent, + lastSuperKeyEvent, + ]) { + if (key != null) { + handleRawKeyEvent(RawKeyUpEvent( + data: key.data, + character: key.character, + )); + } + } + } +} + class ToReleaseKeys { KeyEvent? lastLShiftKeyEvent; KeyEvent? lastRShiftKeyEvent; @@ -229,6 +334,7 @@ class InputModel { var alt = false; var command = false; + final ToReleaseRawKeys toReleaseRawKeys = ToReleaseRawKeys(); final ToReleaseKeys toReleaseKeys = ToReleaseKeys(); // trackpad @@ -361,6 +467,56 @@ class InputModel { } } + KeyEventResult handleRawKeyEvent(RawKeyEvent e) { + if (isViewOnly) return KeyEventResult.handled; + if ((isDesktop || isWebDesktop) && !isInputSourceFlutter) { + return KeyEventResult.handled; + } + + final key = e.logicalKey; + if (e is RawKeyDownEvent) { + if (!e.repeat) { + if (e.isAltPressed && !alt) { + alt = true; + } else if (e.isControlPressed && !ctrl) { + ctrl = true; + } else if (e.isShiftPressed && !shift) { + shift = true; + } else if (e.isMetaPressed && !command) { + command = true; + } + } + toReleaseRawKeys.updateKeyDown(key, e); + } + if (e is RawKeyUpEvent) { + if (key == LogicalKeyboardKey.altLeft || + key == LogicalKeyboardKey.altRight) { + alt = false; + } else if (key == LogicalKeyboardKey.controlLeft || + key == LogicalKeyboardKey.controlRight) { + ctrl = false; + } else if (key == LogicalKeyboardKey.shiftRight || + key == LogicalKeyboardKey.shiftLeft) { + shift = false; + } else if (key == LogicalKeyboardKey.metaLeft || + key == LogicalKeyboardKey.metaRight || + key == LogicalKeyboardKey.superKey) { + command = false; + } + + toReleaseRawKeys.updateKeyUp(key, e); + } + + // * Currently mobile does not enable map mode + if ((isDesktop || isWebDesktop) && keyboardMode == 'map') { + mapKeyboardModeRaw(e); + } else { + legacyKeyboardModeRaw(e); + } + + return KeyEventResult.handled; + } + KeyEventResult handleKeyEvent(KeyEvent e) { if (isViewOnly) return KeyEventResult.handled; if ((isDesktop || isWebDesktop) && !isInputSourceFlutter) { @@ -383,8 +539,10 @@ class InputModel { // * Currently mobile does not enable map mode if ((isDesktop || isWebDesktop)) { // FIXME: e.character is wrong for dead keys, eg: ^ in de - newKeyboardMode(e.character ?? '', e.physicalKey.usbHidUsage & 0xFFFF, - // Show repeat event be converted to "release+press" events? + newKeyboardMode( + e.character ?? '', + e.physicalKey.usbHidUsage & 0xFFFF, + // Show repeat event be converted to "release+press" events? e is KeyDownEvent || e is KeyRepeatEvent); } else { legacyKeyboardMode(e); @@ -419,6 +577,88 @@ class InputModel { downOrUp: down); } + void mapKeyboardModeRaw(RawKeyEvent e) { + int positionCode = -1; + int platformCode = -1; + bool down; + + if (e.data is RawKeyEventDataMacOs) { + RawKeyEventDataMacOs newData = e.data as RawKeyEventDataMacOs; + positionCode = newData.keyCode; + platformCode = newData.keyCode; + } else if (e.data is RawKeyEventDataWindows) { + RawKeyEventDataWindows newData = e.data as RawKeyEventDataWindows; + positionCode = newData.scanCode; + platformCode = newData.keyCode; + } else if (e.data is RawKeyEventDataLinux) { + RawKeyEventDataLinux newData = e.data as RawKeyEventDataLinux; + // scanCode and keyCode of RawKeyEventDataLinux are incorrect. + // 1. scanCode means keycode + // 2. keyCode means keysym + positionCode = newData.scanCode; + platformCode = newData.keyCode; + } else if (e.data is RawKeyEventDataAndroid) { + RawKeyEventDataAndroid newData = e.data as RawKeyEventDataAndroid; + positionCode = newData.scanCode + 8; + platformCode = newData.keyCode; + } else {} + + if (e is RawKeyDownEvent) { + down = true; + } else { + down = false; + } + inputRawKey(e.character ?? '', platformCode, positionCode, down); + } + + /// Send raw Key Event + void inputRawKey(String name, int platformCode, int positionCode, bool down) { + const capslock = 1; + const numlock = 2; + const scrolllock = 3; + int lockModes = 0; + if (HardwareKeyboard.instance.lockModesEnabled + .contains(KeyboardLockMode.capsLock)) { + lockModes |= (1 << capslock); + } + if (HardwareKeyboard.instance.lockModesEnabled + .contains(KeyboardLockMode.numLock)) { + lockModes |= (1 << numlock); + } + if (HardwareKeyboard.instance.lockModesEnabled + .contains(KeyboardLockMode.scrollLock)) { + lockModes |= (1 << scrolllock); + } + bind.sessionHandleFlutterRawKeyEvent( + sessionId: sessionId, + name: name, + platformCode: platformCode, + positionCode: positionCode, + lockModes: lockModes, + downOrUp: down); + } + + void legacyKeyboardModeRaw(RawKeyEvent e) { + if (e is RawKeyDownEvent) { + if (e.repeat) { + sendRawKey(e, press: true); + } else { + sendRawKey(e, down: true); + } + } + if (e is RawKeyUpEvent) { + sendRawKey(e); + } + } + + void sendRawKey(RawKeyEvent e, {bool? down, bool? press}) { + // for maximum compatibility + final label = physicalKeyMap[e.physicalKey.usbHidUsage] ?? + logicalKeyMap[e.logicalKey.keyId] ?? + e.logicalKey.keyLabel; + inputKey(label, down: down, press: press ?? false); + } + void legacyKeyboardMode(KeyEvent e) { if (e is KeyDownEvent) { sendKey(e, down: true); @@ -533,6 +773,7 @@ class InputModel { void enterOrLeave(bool enter) { toReleaseKeys.release(handleKeyEvent); + toReleaseRawKeys.release(handleRawKeyEvent); _pointerMovedAfterEnter = false; // Fix status diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index e310febb150e..48efda3af7a2 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -508,6 +508,27 @@ pub fn session_handle_flutter_key_event( } } +pub fn session_handle_flutter_raw_key_event( + session_id: SessionID, + name: String, + platform_code: i32, + position_code: i32, + lock_modes: i32, + down_or_up: bool, +) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + let keyboard_mode = session.get_keyboard_mode(); + session.handle_flutter_raw_key_event( + &keyboard_mode, + &name, + platform_code, + position_code, + lock_modes, + down_or_up, + ); + } +} + // SyncReturn<()> is used to make sure enter() and leave() are executed in the sequence this function is called. // // If the cursor jumps between remote page of two connections, leave view and enter view will be called. diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 423794be58ee..6686e5419e5d 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -788,7 +788,7 @@ impl Session { } #[cfg(any(target_os = "ios"))] - pub fn handle_flutter_key_event( + pub fn handle_flutter_raw_key_event( &self, _keyboard_mode: &str, _name: &str, @@ -799,6 +799,78 @@ impl Session { ) { } + #[cfg(not(any(target_os = "ios")))] + pub fn handle_flutter_raw_key_event( + &self, + keyboard_mode: &str, + name: &str, + platform_code: i32, + position_code: i32, + lock_modes: i32, + down_or_up: bool, + ) { + if name == "flutter_key" { + self._handle_key_flutter_simulation(keyboard_mode, platform_code, down_or_up); + } else { + self._handle_raw_key_non_flutter_simulation( + keyboard_mode, + platform_code, + position_code, + lock_modes, + down_or_up, + ); + } + } + + #[cfg(not(any(target_os = "ios")))] + fn _handle_raw_key_non_flutter_simulation( + &self, + keyboard_mode: &str, + platform_code: i32, + position_code: i32, + lock_modes: i32, + down_or_up: bool, + ) { + if position_code < 0 || platform_code < 0 { + return; + } + let platform_code: u32 = platform_code as _; + let position_code: KeyCode = position_code as _; + + #[cfg(not(target_os = "windows"))] + let key = rdev::key_from_code(position_code) as rdev::Key; + // Windows requires special handling + #[cfg(target_os = "windows")] + let key = rdev::get_win_key(platform_code, position_code); + + let event_type = if down_or_up { + KeyPress(key) + } else { + KeyRelease(key) + }; + let event = Event { + time: SystemTime::now(), + unicode: None, + platform_code, + position_code: position_code as _, + event_type, + #[cfg(any(target_os = "windows", target_os = "macos"))] + extra_data: 0, + }; + keyboard::client::process_event(keyboard_mode, &event, Some(lock_modes)); + } + + #[cfg(any(target_os = "ios"))] + pub fn handle_flutter_key_event( + &self, + _keyboard_mode: &str, + _character: &str, + _usb_hid: i32, + _lock_modes: i32, + _down_or_up: bool, + ) { + } + #[cfg(not(any(target_os = "ios")))] pub fn handle_flutter_key_event( &self, @@ -870,7 +942,7 @@ impl Session { // We need to set the platform code (keysym) if is AltGr. // https://github.com/rustdesk/rustdesk/blob/07cf1b4db5ef2f925efd3b16b87c33ce03c94809/src/keyboard.rs#L1029 // https://github.com/flutter/flutter/issues/153811 - #[cfg(target_os = "linux")] + #[cfg(target_os = "linux")] let platform_code: u32 = position_code as _; let event_type = if down_or_up { From 24f4b94082135d1cb1fbb356d8a608db350eb91f Mon Sep 17 00:00:00 2001 From: ELForcer <30798063+ELForcer@users.noreply.github.com> Date: Sun, 25 Aug 2024 11:12:08 +0400 Subject: [PATCH 132/541] Update ru.rs (#9163) --- src/lang/ru.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/lang/ru.rs b/src/lang/ru.rs index dc4b2133f9eb..b13c4a1b6634 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -636,13 +636,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Requires at least {} characters", "Требуется не менее {} символов"), ("Wrong PIN", "Неправильный PIN-код"), ("Set PIN", "Установить PIN-код"), - ("Enable trusted devices", ""), - ("Manage trusted devices", ""), - ("Platform", ""), - ("Days remaining", ""), - ("enable-trusted-devices-tip", ""), - ("Parent directory", ""), - ("Resume", ""), - ("Invalid file name", ""), + ("Enable trusted devices", "Включение доверенных устройств"), + ("Manage trusted devices", "Управление доверенными устройствами"), + ("Platform", "Платформа"), + ("Days remaining", "Дней осталось"), + ("enable-trusted-devices-tip", "Разрешить доверенным устройствам пропускать проверку подлинности 2FA"), + ("Parent directory", "Родительская директория"), + ("Resume", "Продолжить"), + ("Invalid file name", "Неверное имя файла"), ].iter().cloned().collect(); } From a946d4d0c9249824a167f5ce4f8a5a1fbdc72cff Mon Sep 17 00:00:00 2001 From: 21pages Date: Sun, 25 Aug 2024 20:46:21 +0800 Subject: [PATCH 133/541] file transfer status text overflow at start (#9166) Signed-off-by: 21pages --- .github/workflows/bridge.yml | 2 +- .github/workflows/flutter-build.yml | 14 +++++++++---- .../lib/desktop/pages/file_manager_page.dart | 6 +++++- flutter/pubspec.lock | 20 +++++++++++++++++-- flutter/pubspec.yaml | 1 + 5 files changed, 35 insertions(+), 8 deletions(-) diff --git a/.github/workflows/bridge.yml b/.github/workflows/bridge.yml index fbaf459a4d81..54180ccdd345 100644 --- a/.github/workflows/bridge.yml +++ b/.github/workflows/bridge.yml @@ -6,7 +6,7 @@ on: workflow_call: env: - FLUTTER_VERSION: "3.16.9" + FLUTTER_VERSION: "3.19.6" FLUTTER_RUST_BRIDGE_VERSION: "1.80.1" RUST_VERSION: "1.75" # https://github.com/rustdesk/rustdesk/discussions/7503 diff --git a/.github/workflows/flutter-build.yml b/.github/workflows/flutter-build.yml index aea6127478cc..d7a36b4005a2 100644 --- a/.github/workflows/flutter-build.yml +++ b/.github/workflows/flutter-build.yml @@ -976,8 +976,11 @@ jobs: - name: fix android for flutter 3.13 if: $${{ env.ANDROID_FLUTTER_VERSION == '3.13.9' }} run: | - sed -i 's/uni_links_desktop/#uni_links_desktop/g' flutter/pubspec.yaml - cd flutter/lib + cd flutter + sed -i 's/uni_links_desktop/#uni_links_desktop/g' pubspec.yaml + sed -i 's/extended_text: .*/extended_text: 11.1.0/' pubspec.yaml + flutter pub get + cd lib find . | grep dart | xargs sed -i 's/textScaler: TextScaler.linear(\(.*\)),/textScaleFactor: \1,/g' - name: Build rustdesk lib @@ -1210,8 +1213,11 @@ jobs: - name: fix android for flutter 3.13 if: $${{ env.ANDROID_FLUTTER_VERSION == '3.13.9' }} run: | - sed -i 's/uni_links_desktop/#uni_links_desktop/g' flutter/pubspec.yaml - cd flutter/lib + cd flutter + sed -i 's/uni_links_desktop/#uni_links_desktop/g' pubspec.yaml + sed -i 's/extended_text: .*/extended_text: 11.1.0/' pubspec.yaml + flutter pub get + cd lib find . | grep dart | xargs sed -i 's/textScaler: TextScaler.linear(\(.*\)),/textScaleFactor: \1,/g' - name: Build rustdesk diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index 05af83cd38d5..c498bf1010b7 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:io'; import 'dart:math'; +import 'package:extended_text/extended_text.dart'; import 'package:flutter_hbb/desktop/widgets/dragable_divider.dart'; import 'package:percent_indicator/percent_indicator.dart'; import 'package:desktop_drop/desktop_drop.dart'; @@ -211,10 +212,13 @@ class _FileManagerPageState extends State Tooltip( waitDuration: Duration(milliseconds: 500), message: item.jobName, - child: Text( + child: ExtendedText( item.jobName, maxLines: 1, overflow: TextOverflow.ellipsis, + overflowWidget: TextOverflowWidget( + child: Text("..."), + position: TextOverflowPosition.start), ), ), Tooltip( diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index bbd91b045ced..61d57bcba4df 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -380,6 +380,22 @@ packages: url: "https://github.com/rustdesk-org/dynamic_layouts.git" source: git version: "0.0.1+1" + extended_text: + dependency: "direct main" + description: + name: extended_text + sha256: "7f382de3af12992e34bd72ddd36becf90c4720900af126cb9859f0189af71ffe" + url: "https://pub.dev" + source: hosted + version: "13.0.0" + extended_text_library: + dependency: transitive + description: + name: extended_text_library + sha256: "55d09098ec56fab0d9a8a68950ca0bbf2efa1327937f7cec6af6dfa066234829" + url: "https://pub.dev" + source: hosted + version: "12.0.0" external_path: dependency: "direct main" description: @@ -1613,5 +1629,5 @@ packages: source: hosted version: "0.2.1" sdks: - dart: ">=3.2.0 <4.0.0" - flutter: ">=3.16.0" + dart: ">=3.3.0 <4.0.0" + flutter: ">=3.19.0" diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index 56022b6ec3f7..db15c74cc0a2 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -104,6 +104,7 @@ dependencies: pull_down_button: ^0.9.3 device_info_plus: ^9.1.0 qr_flutter: ^4.1.0 + extended_text: 13.0.0 dev_dependencies: icons_launcher: ^2.0.4 From 48aec6484c20aa0b00ac496e30916f501bf53c62 Mon Sep 17 00:00:00 2001 From: 21pages Date: Sun, 25 Aug 2024 21:29:41 +0800 Subject: [PATCH 134/541] refresh file transfer table on resume (#9167) Signed-off-by: 21pages --- flutter/lib/desktop/pages/file_manager_page.dart | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index c498bf1010b7..4677744197ed 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -69,7 +69,7 @@ class FileManagerPage extends StatefulWidget { } class _FileManagerPageState extends State - with AutomaticKeepAliveClientMixin { + with AutomaticKeepAliveClientMixin, WidgetsBindingObserver { final _mouseFocusScope = Rx(MouseFocusScope.none); final _dropMaskVisible = false.obs; // TODO impl drop mask @@ -103,6 +103,7 @@ class _FileManagerPageState extends State WidgetsBinding.instance.addPostFrameCallback((_) { widget.tabController.onSelected?.call(widget.id); }); + WidgetsBinding.instance.addObserver(this); } @override @@ -115,12 +116,21 @@ class _FileManagerPageState extends State } Get.delete(tag: 'ft_${widget.id}'); }); + WidgetsBinding.instance.removeObserver(this); super.dispose(); } @override bool get wantKeepAlive => true; + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + super.didChangeAppLifecycleState(state); + if (state == AppLifecycleState.resumed) { + jobController.jobTable.refresh(); + } + } + @override Widget build(BuildContext context) { super.build(context); From 5abe42f66cd907e876360aa9a4090d9b25076346 Mon Sep 17 00:00:00 2001 From: 21pages Date: Mon, 26 Aug 2024 10:37:35 +0800 Subject: [PATCH 135/541] not run get window focus if no multiple displays (#9174) Signed-off-by: 21pages --- src/server/input_service.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/server/input_service.rs b/src/server/input_service.rs index 2f9b86480a2c..06ab61a25c0f 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -385,6 +385,9 @@ fn run_cursor(sp: MouseCursorService, state: &mut StateCursor) -> ResultType<()> fn run_window_focus(sp: EmptyExtraFieldService, state: &mut StateWindowFocus) -> ResultType<()> { let displays = super::display_service::get_sync_displays(); + if displays.len() <= 1 { + return Ok(()); + } let disp_idx = crate::get_focused_display(displays); if let Some(disp_idx) = disp_idx.map(|id| id as i32) { if state.is_changed(disp_idx) { From 4b4fd94f3eb463346e2171fff211fbf43da08cb8 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Mon, 26 Aug 2024 12:13:11 +0800 Subject: [PATCH 136/541] feat: web v2 keyboard (#9175) Signed-off-by: fufesou --- flutter/lib/common.dart | 3 +++ flutter/lib/consts.dart | 1 + .../lib/desktop/widgets/kb_layout_type_chooser.dart | 3 ++- flutter/lib/native/common.dart | 4 ++++ flutter/lib/web/bridge.dart | 13 ++++++++++++- flutter/lib/web/common.dart | 7 +++++++ 6 files changed, 29 insertions(+), 2 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 078bda8a420d..fa1ea11b5951 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -50,6 +50,9 @@ final isLinux = isLinux_; final isDesktop = isDesktop_; final isWeb = isWeb_; final isWebDesktop = isWebDesktop_; +final isWebOnWindows = isWebOnWindows_; +final isWebOnLinux = isWebOnLinux_; +final isWebOnMacOs = isWebOnMacOS_; var isMobile = isAndroid || isIOS; var version = ''; int androidVersion = 0; diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index a5414dd0db02..edc7f427853e 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -32,6 +32,7 @@ const String kPeerPlatformWindows = "Windows"; const String kPeerPlatformLinux = "Linux"; const String kPeerPlatformMacOS = "Mac OS"; const String kPeerPlatformAndroid = "Android"; +const String kPeerPlatformWebDesktop = "WebDesktop"; const double kScrollbarThickness = 12.0; diff --git a/flutter/lib/desktop/widgets/kb_layout_type_chooser.dart b/flutter/lib/desktop/widgets/kb_layout_type_chooser.dart index 7828dd4a0ec6..0984eea5801c 100644 --- a/flutter/lib/desktop/widgets/kb_layout_type_chooser.dart +++ b/flutter/lib/desktop/widgets/kb_layout_type_chooser.dart @@ -178,8 +178,9 @@ String getLocalPlatformForKBLayoutType(String peerPlatform) { localPlatform = kPeerPlatformWindows; } else if (isLinux) { localPlatform = kPeerPlatformLinux; + } else if (isWebOnWindows || isWebOnLinux) { + localPlatform = kPeerPlatformWebDesktop; } - // to-do: web desktop support ? return localPlatform; } diff --git a/flutter/lib/native/common.dart b/flutter/lib/native/common.dart index d3888a245d10..96d5bd6e82ac 100644 --- a/flutter/lib/native/common.dart +++ b/flutter/lib/native/common.dart @@ -11,3 +11,7 @@ final isWebDesktop_ = false; final isDesktop_ = Platform.isWindows || Platform.isMacOS || Platform.isLinux; String get screenInfo_ => ''; + +final isWebOnWindows_ = false; +final isWebOnLinux_ = false; +final isWebOnMacOS_ = false; diff --git a/flutter/lib/web/bridge.dart b/flutter/lib/web/bridge.dart index 5f6f5adef876..8327bdd0ab13 100644 --- a/flutter/lib/web/bridge.dart +++ b/flutter/lib/web/bridge.dart @@ -234,7 +234,7 @@ class RustdeskImpl { } String getLocalKbLayoutType({dynamic hint}) { - throw js.context.callMethod('getByName', ['option:local', 'kb_layout']); + return js.context.callMethod('getByName', ['option:local', 'kb_layout']); } Future setLocalKbLayoutType( @@ -415,6 +415,17 @@ class RustdeskImpl { ])); } + Future sessionHandleFlutterRawKeyEvent( + {required UuidValue sessionId, + required String name, + required int platformCode, + required int positionCode, + required int lockModes, + required bool downOrUp, + dynamic hint}) { + throw UnimplementedError(); + } + void sessionEnterOrLeave( {required UuidValue sessionId, required bool enter, dynamic hint}) { throw UnimplementedError(); diff --git a/flutter/lib/web/common.dart b/flutter/lib/web/common.dart index 93b53f948021..0f3a996816d5 100644 --- a/flutter/lib/web/common.dart +++ b/flutter/lib/web/common.dart @@ -1,4 +1,5 @@ import 'dart:js' as js; +import 'dart:html' as html; final isAndroid_ = false; final isIOS_ = false; @@ -11,3 +12,9 @@ final isWebDesktop_ = !js.context.callMethod('isMobile'); final isDesktop_ = false; String get screenInfo_ => js.context.callMethod('getByName', ['screen_info']); + +final _userAgent = html.window.navigator.userAgent.toLowerCase(); + +final isWebOnWindows_ = _userAgent.contains('win'); +final isWebOnLinux_ = _userAgent.contains('linux'); +final isWebOnMacOS_ = _userAgent.contains('mac'); From 690a2c8399f9724754b77ddd11026096c9038ad1 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Mon, 26 Aug 2024 17:07:02 +0800 Subject: [PATCH 137/541] still find delegate failure when my mac restarted automatically sometimes --- src/platform/macos.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/platform/macos.rs b/src/platform/macos.rs index 3d14485d2694..bb803bdaae7f 100644 --- a/src/platform/macos.rs +++ b/src/platform/macos.rs @@ -507,6 +507,10 @@ pub fn start_os_service() { .map(|p| p.start_time()) .unwrap_or_default() as i64; log::info!("Startime: {my_start_time} vs {:?}", server); + if my_start_time < server.unwrap().0 + 3 { + log::error!("Please start --server first to make delegate work, earlier more 3 seconds",); + std::process::exit(-1); + } std::thread::spawn(move || loop { std::thread::sleep(std::time::Duration::from_secs(1)); @@ -519,9 +523,9 @@ pub fn start_os_service() { ); std::process::exit(-1); }; - if my_start_time <= start_time + 1 { + if my_start_time < start_time + 3 { log::error!( - "Agent start later, {my_start_time} vs {start_time}, please start --server first to make delegate work", + "Agent start later, {my_start_time} vs {start_time}, please start --server first to make delegate work, earlier more 3 seconds", ); std::process::exit(-1); } From c68ce7dd84db405b93b5fa411a5e033b71ef75d2 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Tue, 27 Aug 2024 00:00:33 +0800 Subject: [PATCH 138/541] fix: web v2, keyboard mode (#9180) Signed-off-by: fufesou --- flutter/lib/desktop/widgets/remote_toolbar.dart | 4 +++- flutter/lib/models/input_model.dart | 4 ++-- flutter/lib/web/bridge.dart | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/flutter/lib/desktop/widgets/remote_toolbar.dart b/flutter/lib/desktop/widgets/remote_toolbar.dart index 98fa676144b4..af177f840503 100644 --- a/flutter/lib/desktop/widgets/remote_toolbar.dart +++ b/flutter/lib/desktop/widgets/remote_toolbar.dart @@ -1612,7 +1612,9 @@ class _KeyboardMenu extends StatelessWidget { // If use flutter to grab keys, we can only use one mode. // Map mode and Legacy mode, at least one of them is supported. String? modeOnly; - if (isInputSourceFlutter) { + // Keep both map and legacy mode on web at the moment. + // TODO: Remove legacy mode after web supports translate mode on web. + if (isInputSourceFlutter && isDesktop) { if (bind.sessionIsKeyboardModeSupported( sessionId: ffi.sessionId, mode: kKeyMapMode)) { modeOnly = kKeyMapMode; diff --git a/flutter/lib/models/input_model.dart b/flutter/lib/models/input_model.dart index 54e7a3f4dc04..4cdbf88e5f06 100644 --- a/flutter/lib/models/input_model.dart +++ b/flutter/lib/models/input_model.dart @@ -508,7 +508,7 @@ class InputModel { } // * Currently mobile does not enable map mode - if ((isDesktop || isWebDesktop) && keyboardMode == 'map') { + if ((isDesktop || isWebDesktop) && keyboardMode == kKeyMapMode) { mapKeyboardModeRaw(e); } else { legacyKeyboardModeRaw(e); @@ -537,7 +537,7 @@ class InputModel { } // * Currently mobile does not enable map mode - if ((isDesktop || isWebDesktop)) { + if ((isDesktop || isWebDesktop) && keyboardMode == kKeyMapMode) { // FIXME: e.character is wrong for dead keys, eg: ^ in de newKeyboardMode( e.character ?? '', diff --git a/flutter/lib/web/bridge.dart b/flutter/lib/web/bridge.dart index 8327bdd0ab13..3aa65a5be69f 100644 --- a/flutter/lib/web/bridge.dart +++ b/flutter/lib/web/bridge.dart @@ -351,7 +351,7 @@ class RustdeskImpl { bool sessionIsKeyboardModeSupported( {required UuidValue sessionId, required String mode, dynamic hint}) { - return mode == kKeyLegacyMode; + return [kKeyLegacyMode, kKeyMapMode].contains(mode); } bool sessionIsMultiUiSession({required UuidValue sessionId, dynamic hint}) { From 40239a1c414158a3248bc400d9dca049292848e4 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Tue, 27 Aug 2024 15:20:29 +0800 Subject: [PATCH 139/541] feat: macos, mouse button, back&forward (#9185) Signed-off-by: fufesou --- libs/enigo/src/macos/macos_impl.rs | 57 ++++++++++++++++++++++-------- 1 file changed, 42 insertions(+), 15 deletions(-) diff --git a/libs/enigo/src/macos/macos_impl.rs b/libs/enigo/src/macos/macos_impl.rs index b56beff129f7..a1f5d2e4a9fb 100644 --- a/libs/enigo/src/macos/macos_impl.rs +++ b/libs/enigo/src/macos/macos_impl.rs @@ -37,6 +37,9 @@ const kUCKeyActionDisplay: u16 = 3; const kUCKeyTranslateDeadKeysBit: OptionBits = 1 << 31; const BUF_LEN: usize = 4; +const MOUSE_EVENT_BUTTON_NUMBER_BACK: i64 = 3; +const MOUSE_EVENT_BUTTON_NUMBER_FORWARD: i64 = 4; + /// The event source user data value of cgevent. pub const ENIGO_INPUT_EXTRA_VALUE: i64 = 100; @@ -226,14 +229,24 @@ impl MouseControllable for Enigo { } self.last_click_time = Some(now); let (current_x, current_y) = Self::mouse_location(); - let (button, event_type) = match button { - MouseButton::Left => (CGMouseButton::Left, CGEventType::LeftMouseDown), - MouseButton::Middle => (CGMouseButton::Center, CGEventType::OtherMouseDown), - MouseButton::Right => (CGMouseButton::Right, CGEventType::RightMouseDown), + let (button, event_type, btn_value) = match button { + MouseButton::Left => (CGMouseButton::Left, CGEventType::LeftMouseDown, None), + MouseButton::Middle => (CGMouseButton::Center, CGEventType::OtherMouseDown, None), + MouseButton::Right => (CGMouseButton::Right, CGEventType::RightMouseDown, None), + MouseButton::Back => ( + CGMouseButton::Left, + CGEventType::OtherMouseDown, + Some(MOUSE_EVENT_BUTTON_NUMBER_BACK), + ), + MouseButton::Forward => ( + CGMouseButton::Left, + CGEventType::OtherMouseDown, + Some(MOUSE_EVENT_BUTTON_NUMBER_FORWARD), + ), _ => { log::info!("Unsupported button {:?}", button); return Ok(()); - }, + } }; let dest = CGPoint::new(current_x as f64, current_y as f64); if let Some(src) = self.event_source.as_ref() { @@ -244,6 +257,9 @@ impl MouseControllable for Enigo { self.multiple_click, ); } + if let Some(v) = btn_value { + event.set_integer_value_field(EventField::MOUSE_EVENT_BUTTON_NUMBER, v); + } self.post(event); } } @@ -252,14 +268,24 @@ impl MouseControllable for Enigo { fn mouse_up(&mut self, button: MouseButton) { let (current_x, current_y) = Self::mouse_location(); - let (button, event_type) = match button { - MouseButton::Left => (CGMouseButton::Left, CGEventType::LeftMouseUp), - MouseButton::Middle => (CGMouseButton::Center, CGEventType::OtherMouseUp), - MouseButton::Right => (CGMouseButton::Right, CGEventType::RightMouseUp), + let (button, event_type, btn_value) = match button { + MouseButton::Left => (CGMouseButton::Left, CGEventType::LeftMouseUp, None), + MouseButton::Middle => (CGMouseButton::Center, CGEventType::OtherMouseUp, None), + MouseButton::Right => (CGMouseButton::Right, CGEventType::RightMouseUp, None), + MouseButton::Back => ( + CGMouseButton::Left, + CGEventType::OtherMouseUp, + Some(MOUSE_EVENT_BUTTON_NUMBER_BACK), + ), + MouseButton::Forward => ( + CGMouseButton::Left, + CGEventType::OtherMouseUp, + Some(MOUSE_EVENT_BUTTON_NUMBER_FORWARD), + ), _ => { log::info!("Unsupported button {:?}", button); return; - }, + } }; let dest = CGPoint::new(current_x as f64, current_y as f64); if let Some(src) = self.event_source.as_ref() { @@ -270,6 +296,9 @@ impl MouseControllable for Enigo { self.multiple_click, ); } + if let Some(v) = btn_value { + event.set_integer_value_field(EventField::MOUSE_EVENT_BUTTON_NUMBER, v); + } self.post(event); } } @@ -345,7 +374,7 @@ impl KeyboardControllable for Enigo { fn as_mut_any(&mut self) -> &mut dyn std::any::Any { self } - + fn key_sequence(&mut self, sequence: &str) { // NOTE(dustin): This is a fix for issue https://github.com/enigo-rs/enigo/issues/68 // TODO(dustin): This could be improved by aggregating 20 bytes worth of graphemes at a time @@ -382,12 +411,10 @@ impl KeyboardControllable for Enigo { fn key_down(&mut self, key: Key) -> crate::ResultType { let code = self.key_to_keycode(key); if code == u16::MAX { - return Err("".into()); + return Err("".into()); } if let Some(src) = self.event_source.as_ref() { - if let Ok(event) = - CGEvent::new_keyboard_event(src.clone(), code, true) - { + if let Ok(event) = CGEvent::new_keyboard_event(src.clone(), code, true) { self.post(event); } } From 55de573a0162f0dc39dd516ccd878513a1a08b33 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Tue, 27 Aug 2024 19:30:51 +0800 Subject: [PATCH 140/541] fix: keyboard input, mulit windows (#9189) Signed-off-by: fufesou --- flutter/lib/models/input_model.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/flutter/lib/models/input_model.dart b/flutter/lib/models/input_model.dart index 4cdbf88e5f06..fd1beb6b5d69 100644 --- a/flutter/lib/models/input_model.dart +++ b/flutter/lib/models/input_model.dart @@ -784,6 +784,9 @@ class InputModel { if (!isInputSourceFlutter) { bind.sessionEnterOrLeave(sessionId: sessionId, enter: enter); } + if (enter) { + bind.setCurSessionId(sessionId: sessionId); + } } /// Send mouse movement event with distance in [x] and [y]. From f3a2733d75d141b64f411be342b62325b38e2d22 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Tue, 27 Aug 2024 20:25:01 +0800 Subject: [PATCH 141/541] start mac service more later --- src/platform/macos.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/platform/macos.rs b/src/platform/macos.rs index bb803bdaae7f..71a1022db445 100644 --- a/src/platform/macos.rs +++ b/src/platform/macos.rs @@ -507,10 +507,6 @@ pub fn start_os_service() { .map(|p| p.start_time()) .unwrap_or_default() as i64; log::info!("Startime: {my_start_time} vs {:?}", server); - if my_start_time < server.unwrap().0 + 3 { - log::error!("Please start --server first to make delegate work, earlier more 3 seconds",); - std::process::exit(-1); - } std::thread::spawn(move || loop { std::thread::sleep(std::time::Duration::from_secs(1)); @@ -523,7 +519,7 @@ pub fn start_os_service() { ); std::process::exit(-1); }; - if my_start_time < start_time + 3 { + if my_start_time <= start_time + 3 { log::error!( "Agent start later, {my_start_time} vs {start_time}, please start --server first to make delegate work, earlier more 3 seconds", ); From fd178a7b6c1315516c61016107a4a17377aaec37 Mon Sep 17 00:00:00 2001 From: 9amm Date: Tue, 27 Aug 2024 17:38:40 +0200 Subject: [PATCH 142/541] Fix minor typo (#9191) Co-authored-by: 9amm <> --- src/lang/es.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lang/es.rs b/src/lang/es.rs index 579d770bd500..794c751c00f3 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -394,7 +394,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Accept sessions via click", "Aceptar sesiones a través de clic"), ("Accept sessions via both", "Aceptar sesiones a través de ambos"), ("Please wait for the remote side to accept your session request...", "Por favor, espere a que el lado remoto acepte su solicitud de sesión"), - ("One-time Password", "Constaseña de un solo uso"), + ("One-time Password", "Contraseña de un solo uso"), ("Use one-time password", "Usar contraseña de un solo uso"), ("One-time password length", "Longitud de la contraseña de un solo uso"), ("Request access to your device", "Solicitud de acceso a su dispositivo"), From cf06d1028f1d0f605b4a3c1163f27086547590f3 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Tue, 27 Aug 2024 23:58:04 +0800 Subject: [PATCH 143/541] fix: web, reset cursor on disconn, back to main page (#9192) Signed-off-by: fufesou --- flutter/lib/models/input_model.dart | 2 +- flutter/lib/models/model.dart | 1 + flutter/lib/native/custom_cursor.dart | 1 + flutter/lib/web/custom_cursor.dart | 6 ++++++ 4 files changed, 9 insertions(+), 1 deletion(-) diff --git a/flutter/lib/models/input_model.dart b/flutter/lib/models/input_model.dart index fd1beb6b5d69..ac78e17309b8 100644 --- a/flutter/lib/models/input_model.dart +++ b/flutter/lib/models/input_model.dart @@ -784,7 +784,7 @@ class InputModel { if (!isInputSourceFlutter) { bind.sessionEnterOrLeave(sessionId: sessionId, enter: enter); } - if (enter) { + if (!isWeb && enter) { bind.setCurSessionId(sessionId: sessionId); } } diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index d5377ea9a450..6f2a9eb2e2d1 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -2183,6 +2183,7 @@ class CursorModel with ChangeNotifier { debugPrint("deleting cursor with key $k"); deleteCustomCursor(k); } + resetSystemCursor(); } trySetRemoteWindowCoords() { diff --git a/flutter/lib/native/custom_cursor.dart b/flutter/lib/native/custom_cursor.dart index 3e53f3cc5b25..e85d42a55893 100644 --- a/flutter/lib/native/custom_cursor.dart +++ b/flutter/lib/native/custom_cursor.dart @@ -9,6 +9,7 @@ import 'package:flutter_hbb/models/model.dart'; deleteCustomCursor(String key) => custom_cursor_manager.CursorManager.instance.deleteCursor(key); +resetSystemCursor() {} MouseCursor buildCursorOfCache( CursorModel cursor, double scale, CursorData? cache) { diff --git a/flutter/lib/web/custom_cursor.dart b/flutter/lib/web/custom_cursor.dart index fd1fc4a18f85..54df77e98b5e 100644 --- a/flutter/lib/web/custom_cursor.dart +++ b/flutter/lib/web/custom_cursor.dart @@ -58,6 +58,11 @@ class CursorManager { ]); } } + + Future resetSystemCursor() async { + latestKey = ''; + js.context.callMethod('setByName', ['cursor', 'auto']); + } } class FlutterCustomMemoryImageCursor extends MouseCursor { @@ -92,6 +97,7 @@ class _FlutterCustomMemoryImageCursorSession extends MouseCursorSession { } deleteCustomCursor(String key) => CursorManager.instance.deleteCursor(key); +resetSystemCursor() => CursorManager.instance.resetSystemCursor(); MouseCursor buildCursorOfCache( model.CursorModel cursor, double scale, model.CursorData? cache) { From 6a5d5875c85125f0535770c7a4199c16ff2add72 Mon Sep 17 00:00:00 2001 From: RustDesk <71636191+rustdesk@users.noreply.github.com> Date: Wed, 28 Aug 2024 12:23:40 +0800 Subject: [PATCH 144/541] Logo broken (#9195) --- README.md | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/README.md b/README.md index daf4a590743b..02c1eee75959 100644 --- a/README.md +++ b/README.md @@ -174,10 +174,4 @@ Please ensure that you are running these commands from the root of the RustDesk ## [Public Servers](#public-servers) -RustDesk is supported by a free EU server, graciously provided by Codext GmbH - -

- - Codext GmbH - -

\ No newline at end of file +RustDesk is supported by a free EU server, graciously provided by [Codext GmbH](https://codext.link/rustdesk?utm_source=github) From d335cdbb0c9f840f8ad2518497025670b6bc9cf5 Mon Sep 17 00:00:00 2001 From: RustDesk <71636191+rustdesk@users.noreply.github.com> Date: Wed, 28 Aug 2024 12:25:00 +0800 Subject: [PATCH 145/541] Update README.md (#9196) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 02c1eee75959..c193967d0b56 100644 --- a/README.md +++ b/README.md @@ -174,4 +174,4 @@ Please ensure that you are running these commands from the root of the RustDesk ## [Public Servers](#public-servers) -RustDesk is supported by a free EU server, graciously provided by [Codext GmbH](https://codext.link/rustdesk?utm_source=github) +RustDesk is supported by a free EU server, graciously provided by [Codext GmbH](https://codext.link/rustdesk?utm_source=github) From 832002a10f51142ee38ee09ea14210af05c89a7d Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Thu, 29 Aug 2024 23:38:08 +0800 Subject: [PATCH 146/541] refact: web, remote toolbar, pin (#9206) Signed-off-by: fufesou --- flutter/lib/desktop/widgets/remote_toolbar.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/lib/desktop/widgets/remote_toolbar.dart b/flutter/lib/desktop/widgets/remote_toolbar.dart index af177f840503..f881bc84007c 100644 --- a/flutter/lib/desktop/widgets/remote_toolbar.dart +++ b/flutter/lib/desktop/widgets/remote_toolbar.dart @@ -452,8 +452,8 @@ class _RemoteToolbarState extends State { Widget _buildToolbar(BuildContext context) { final List toolbarItems = []; + toolbarItems.add(_PinMenu(state: widget.state)); if (!isWebDesktop) { - toolbarItems.add(_PinMenu(state: widget.state)); toolbarItems.add(_MobileActionMenu(ffi: widget.ffi)); } From e3f6829d025152adb1d1fcac7d9d38dfee4f2cf3 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Fri, 30 Aug 2024 00:37:38 +0800 Subject: [PATCH 147/541] refact: android ios, lan discovery (#9207) * refact: android ios, lan discovery Signed-off-by: fufesou * fix: build and runtime error Signed-off-by: fufesou --------- Signed-off-by: fufesou --- src/lan.rs | 38 +++++++++++++++++++++++++++++++------- src/lib.rs | 2 -- src/platform/mod.rs | 1 + src/rendezvous_mediator.rs | 5 ++++- src/ui_interface.rs | 1 - 5 files changed, 36 insertions(+), 11 deletions(-) diff --git a/src/lan.rs b/src/lan.rs index 666e73c7c3dc..7d3f4f05fedd 100644 --- a/src/lan.rs +++ b/src/lan.rs @@ -1,4 +1,3 @@ -#[cfg(not(any(target_os = "android", target_os = "ios")))] use hbb_common::config::Config; use hbb_common::{ allow_err, @@ -22,7 +21,7 @@ use std::{ type Message = RendezvousMessage; -#[cfg(not(any(target_os = "android", target_os = "ios")))] +#[cfg(not(target_os = "ios"))] pub(super) fn start_listening() -> ResultType<()> { let addr = SocketAddr::from(([0, 0, 0, 0], get_broadcast_port())); let socket = std::net::UdpSocket::bind(addr)?; @@ -40,13 +39,22 @@ pub(super) fn start_listening() -> ResultType<()> { &Config::get_option("enable-lan-discovery"), ) { + let id = Config::get_id(); + if p.id == id { + continue; + } if let Some(self_addr) = get_ipaddr_by_peer(&addr) { let mut msg_out = Message::new(); + let mut hostname = whoami::hostname(); + // The default hostname is "localhost" which is a bit confusing + if hostname == "localhost" { + hostname = "unknown".to_owned(); + } let peer = PeerDiscovery { cmd: "pong".to_owned(), mac: get_mac(&self_addr), - id: Config::get_id(), - hostname: whoami::hostname(), + id, + hostname, username: crate::platform::get_active_username(), platform: whoami::platform().to_string(), ..Default::default() @@ -100,17 +108,17 @@ fn get_broadcast_port() -> u16 { } fn get_mac(_ip: &IpAddr) -> String { - #[cfg(not(any(target_os = "android", target_os = "ios")))] + #[cfg(not(target_os = "ios"))] if let Ok(mac) = get_mac_by_ip(_ip) { mac.to_string() } else { "".to_owned() } - #[cfg(any(target_os = "android", target_os = "ios"))] + #[cfg(target_os = "ios")] "".to_owned() } -#[cfg(not(any(target_os = "android", target_os = "ios")))] +#[cfg(not(target_os = "ios"))] fn get_mac_by_ip(ip: &IpAddr) -> ResultType { for interface in default_net::get_interfaces() { match ip { @@ -153,6 +161,10 @@ fn get_ipaddr_by_peer(peer: A) -> Option { fn create_broadcast_sockets() -> Vec { let mut ipv4s = Vec::new(); + // TODO: maybe we should use a better way to get ipv4 addresses. + // But currently, it's ok to use `[Ipv4Addr::UNSPECIFIED]` for discovery. + // `default_net::get_interfaces()` causes undefined symbols error when `flutter build` on iOS simulator x86_64 + #[cfg(not(any(target_os = "ios")))] for interface in default_net::get_interfaces() { for ipv4 in &interface.ipv4 { ipv4s.push(ipv4.addr.clone()); @@ -178,8 +190,20 @@ fn send_query() -> ResultType> { } let mut msg_out = Message::new(); + // We may not be able to get the mac address on mobile platforms. + // So we need to use the id to avoid discovering ourselves. + #[cfg(any(target_os = "android", target_os = "ios"))] + let id = crate::ui_interface::get_id(); + // `crate::ui_interface::get_id()` will cause error: + // `get_id()` uses async code with `current_thread`, which is not allowed in this context. + // + // No need to get id for desktop platforms. + // We can use the mac address to identify the device. + #[cfg(not(any(target_os = "android", target_os = "ios")))] + let id = "".to_owned(); let peer = PeerDiscovery { cmd: "ping".to_owned(), + id, ..Default::default() }; msg_out.set_peer_discovery(peer); diff --git a/src/lib.rs b/src/lib.rs index f8d917a518e2..7f9ca4e9aafa 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,4 @@ mod keyboard; -#[cfg(not(any(target_os = "ios")))] /// cbindgen:ignore pub mod platform; #[cfg(not(any(target_os = "android", target_os = "ios")))] @@ -12,7 +11,6 @@ mod server; #[cfg(not(any(target_os = "ios")))] pub use self::server::*; mod client; -#[cfg(not(any(target_os = "ios")))] mod lan; #[cfg(not(any(target_os = "ios")))] mod rendezvous_mediator; diff --git a/src/platform/mod.rs b/src/platform/mod.rs index fe66e50dc694..7bb503fdd5c5 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -100,6 +100,7 @@ impl WakeLock { } } +#[cfg(not(target_os = "ios"))] pub fn get_wakelock(_display: bool) -> WakeLock { hbb_common::log::info!("new wakelock, require display on: {_display}"); #[cfg(target_os = "android")] diff --git a/src/rendezvous_mediator.rs b/src/rendezvous_mediator.rs index bd628ec66ef4..f5d81eaffa63 100644 --- a/src/rendezvous_mediator.rs +++ b/src/rendezvous_mediator.rs @@ -76,8 +76,11 @@ impl RendezvousMediator { tokio::spawn(async move { direct_server(server_cloned).await; }); + #[cfg(target_os = "android")] + let start_lan_listening = true; #[cfg(not(any(target_os = "android", target_os = "ios")))] - if crate::platform::is_installed() { + let start_lan_listening = crate::platform::is_installed(); + if start_lan_listening { std::thread::spawn(move || { allow_err!(super::lan::start_listening()); }); diff --git a/src/ui_interface.rs b/src/ui_interface.rs index 9c3864f0c3c9..8489179ba67b 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -692,7 +692,6 @@ pub fn create_shortcut(_id: String) { #[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))] #[inline] pub fn discover() { - #[cfg(not(any(target_os = "ios")))] std::thread::spawn(move || { allow_err!(crate::lan::discover()); }); From bf390611ab1c44c3492d329ae949ebed15ce3b60 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Sat, 31 Aug 2024 19:02:50 +0800 Subject: [PATCH 148/541] fix: keyboard, sciter (#9216) Signed-off-by: fufesou --- src/flutter_ffi.rs | 14 ++------------ src/keyboard.rs | 2 ++ src/ui/header.tis | 7 +++++-- src/ui/remote.rs | 1 + src/ui_session_interface.rs | 12 ++++++++++++ 5 files changed, 22 insertions(+), 14 deletions(-) diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 48efda3af7a2..8cc475316142 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -1,6 +1,6 @@ use crate::{ client::file_trait::FileManager, - common::{is_keyboard_mode_supported, make_fd_to_json}, + common::make_fd_to_json, flutter::{ self, session_add, session_add_existed, session_start_, sessions, try_sync_peer_option, }, @@ -19,13 +19,11 @@ use hbb_common::allow_err; use hbb_common::{ config::{self, LocalConfig, PeerConfig, PeerInfoSerde}, fs, lazy_static, log, - message_proto::KeyboardMode, rendezvous_proto::ConnType, ResultType, }; use std::{ collections::HashMap, - str::FromStr, sync::{ atomic::{AtomicI32, Ordering}, Arc, @@ -447,15 +445,7 @@ pub fn session_get_custom_image_quality(session_id: SessionID) -> Option SyncReturn { if let Some(session) = sessions::get_session_by_session_id(&session_id) { - if let Ok(mode) = KeyboardMode::from_str(&mode[..]) { - SyncReturn(is_keyboard_mode_supported( - &mode, - session.get_peer_version(), - &session.peer_platform(), - )) - } else { - SyncReturn(false) - } + SyncReturn(session.is_keyboard_mode_supported(mode)) } else { SyncReturn(false) } diff --git a/src/keyboard.rs b/src/keyboard.rs index 6b4b0988fb23..e9bff5d8118d 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -34,6 +34,7 @@ const OS_LOWER_ANDROID: &str = "android"; #[cfg(any(target_os = "windows", target_os = "macos"))] static KEYBOARD_HOOKED: AtomicBool = AtomicBool::new(false); +#[cfg(feature = "flutter")] #[cfg(not(any(target_os = "android", target_os = "ios")))] static IS_RDEV_ENABLED: AtomicBool = AtomicBool::new(false); @@ -71,6 +72,7 @@ pub mod client { #[cfg(not(any(target_os = "android", target_os = "ios")))] pub fn change_grab_status(state: GrabState, keyboard_mode: &str) { + #[cfg(feature = "flutter")] if !IS_RDEV_ENABLED.load(Ordering::SeqCst) { return; } diff --git a/src/ui/header.tis b/src/ui/header.tis index c4e765280970..e99d398aa73d 100644 --- a/src/ui/header.tis +++ b/src/ui/header.tis @@ -152,10 +152,13 @@ class Header: Reactor.Component { } function renderKeyboardPop(){ + const is_map_mode_supported = handler.is_keyboard_mode_supported("map"); + const is_translate_mode_supported = handler.is_keyboard_mode_supported("translate"); return -
  • {svg_checkmark}{translate('Legacy mode')}
  • -
  • {svg_checkmark}{translate('Map mode')}
  • +
  • {svg_checkmark}{translate('Legacy mode')}
  • + { is_map_mode_supported &&
  • {svg_checkmark}{translate('Map mode')}
  • } + { is_translate_mode_supported &&
  • {svg_checkmark}{translate('Translate mode')}
  • }
    ; } diff --git a/src/ui/remote.rs b/src/ui/remote.rs index 0ad84e8ed15a..baf9d1f64512 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -487,6 +487,7 @@ impl sciter::EventHandler for SciterSession { fn peer_platform(); fn set_write_override(i32, i32, bool, bool, bool); fn get_keyboard_mode(); + fn is_keyboard_mode_supported(String); fn save_keyboard_mode(String); fn alternative_codecs(); fn change_prefer_codec(); diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 6686e5419e5d..ce28b78d8e95 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -252,6 +252,18 @@ impl Session { self.fallback_keyboard_mode() } + pub fn is_keyboard_mode_supported(&self, mode: String) -> bool { + if let Ok(mode) = KeyboardMode::from_str(&mode[..]) { + crate::common::is_keyboard_mode_supported( + &mode, + self.get_peer_version(), + &self.peer_platform(), + ) + } else { + false + } + } + pub fn save_keyboard_mode(&self, value: String) { self.lc.write().unwrap().save_keyboard_mode(value); } From ae339f039df1b5acdc149b8079a0d77d04c4f132 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Sun, 1 Sep 2024 00:30:07 +0800 Subject: [PATCH 149/541] refact: web ui (#9217) * refact: web ui Signed-off-by: fufesou * refact: remove AppBar shadow Signed-off-by: fufesou --------- Signed-off-by: fufesou --- flutter/lib/common.dart | 10 +- .../desktop/pages/desktop_setting_page.dart | 104 +++++++++++------- flutter/lib/mobile/pages/connection_page.dart | 77 +------------ flutter/lib/mobile/pages/home_page.dart | 9 +- flutter/lib/web/bridge.dart | 16 +-- flutter/lib/web/settings_page.dart | 98 +++++++++++++++++ 6 files changed, 188 insertions(+), 126 deletions(-) create mode 100644 flutter/lib/web/settings_page.dart diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index fa1ea11b5951..efcff4f83f4c 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -350,6 +350,9 @@ class MyTheme { hoverColor: Color.fromARGB(255, 224, 224, 224), scaffoldBackgroundColor: Colors.white, dialogBackgroundColor: Colors.white, + appBarTheme: AppBarTheme( + shadowColor: Colors.transparent, + ), dialogTheme: DialogTheme( elevation: 15, shape: RoundedRectangleBorder( @@ -445,6 +448,9 @@ class MyTheme { hoverColor: Color.fromARGB(255, 45, 46, 53), scaffoldBackgroundColor: Color(0xFF18191E), dialogBackgroundColor: Color(0xFF18191E), + appBarTheme: AppBarTheme( + shadowColor: Colors.transparent, + ), dialogTheme: DialogTheme( elevation: 15, shape: RoundedRectangleBorder( @@ -550,7 +556,7 @@ class MyTheme { static void changeDarkMode(ThemeMode mode) async { Get.changeThemeMode(mode); - if (desktopType == DesktopType.main || isAndroid || isIOS) { + if (desktopType == DesktopType.main || isAndroid || isIOS || isWeb) { if (mode == ThemeMode.system) { await bind.mainSetLocalOption( key: kCommConfKeyTheme, value: defaultOptionTheme); @@ -558,7 +564,7 @@ class MyTheme { await bind.mainSetLocalOption( key: kCommConfKeyTheme, value: mode.toShortString()); } - await bind.mainChangeTheme(dark: mode.toShortString()); + if (!isWeb) await bind.mainChangeTheme(dark: mode.toShortString()); // Synchronize the window theme of the system. updateSystemWindowTheme(); } diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 6d9f92b59dc4..4ab1a8c56671 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -61,7 +61,8 @@ class DesktopSettingPage extends StatefulWidget { final SettingsTabKey initialTabkey; static final List tabKeys = [ SettingsTabKey.general, - if (!bind.isOutgoingOnly() && + if (!isWeb && + !bind.isOutgoingOnly() && !bind.isDisableSettings() && bind.mainGetBuildinOption(key: kOptionHideSecuritySetting) != 'Y') SettingsTabKey.safety, @@ -216,7 +217,7 @@ class _DesktopSettingPageState extends State width: _kTabWidth, child: Column( children: [ - _header(), + _header(context), Flexible(child: _listView(tabs: _settingTabs())), ], ), @@ -239,21 +240,40 @@ class _DesktopSettingPageState extends State ); } - Widget _header() { + Widget _header(BuildContext context) { + final settingsText = Text( + translate('Settings'), + textAlign: TextAlign.left, + style: const TextStyle( + color: _accentColor, + fontSize: _kTitleFontSize, + fontWeight: FontWeight.w400, + ), + ); return Row( children: [ - SizedBox( - height: 62, - child: Text( - translate('Settings'), - textAlign: TextAlign.left, - style: const TextStyle( - color: _accentColor, - fontSize: _kTitleFontSize, - fontWeight: FontWeight.w400, + if (isWeb) + IconButton( + onPressed: () { + if (Navigator.canPop(context)) { + Navigator.pop(context); + } + }, + icon: Icon(Icons.arrow_back), + ).marginOnly(left: 5), + if (isWeb) + SizedBox( + height: 62, + child: Align( + alignment: Alignment.center, + child: settingsText, ), - ), - ).marginOnly(left: 20, top: 10), + ).marginOnly(left: 20), + if (!isWeb) + SizedBox( + height: 62, + child: settingsText, + ).marginOnly(left: 20, top: 10), const Spacer(), ], ); @@ -322,7 +342,8 @@ class _General extends StatefulWidget { } class _GeneralState extends State<_General> { - final RxBool serviceStop = Get.find(tag: 'stop-service'); + final RxBool serviceStop = + isWeb ? RxBool(false) : Get.find(tag: 'stop-service'); RxBool serviceBtnEnabled = true.obs; @override @@ -334,13 +355,13 @@ class _GeneralState extends State<_General> { physics: DraggableNeverScrollableScrollPhysics(), controller: scrollController, children: [ - service(), + if (!isWeb) service(), theme(), _Card(title: 'Language', children: [language()]), - hwcodec(), - audio(context), - record(context), - WaylandCard(), + if (!isWeb) hwcodec(), + if (!isWeb) audio(context), + if (!isWeb) record(context), + if (!isWeb) WaylandCard(), other() ], ).marginOnly(bottom: _kListViewBottomMargin)); @@ -394,13 +415,13 @@ class _GeneralState extends State<_General> { Widget other() { final children = [ - if (!bind.isIncomingOnly()) + if (!isWeb && !bind.isIncomingOnly()) _OptionCheckBox(context, 'Confirm before closing multiple tabs', kOptionEnableConfirmClosingTabs, isServer: false), _OptionCheckBox(context, 'Adaptive bitrate', kOptionEnableAbr), - wallpaper(), - if (!bind.isIncomingOnly()) ...[ + if (!isWeb) wallpaper(), + if (!isWeb && !bind.isIncomingOnly()) ...[ _OptionCheckBox( context, 'Open connection in new tab', @@ -417,18 +438,19 @@ class _GeneralState extends State<_General> { kOptionAllowAlwaysSoftwareRender, ), ), - Tooltip( - message: translate('texture_render_tip'), - child: _OptionCheckBox( - context, - "Use texture rendering", - kOptionTextureRender, - optGetter: bind.mainGetUseTextureRender, - optSetter: (k, v) async => - await bind.mainSetLocalOption(key: k, value: v ? 'Y' : 'N'), + if (!isWeb) + Tooltip( + message: translate('texture_render_tip'), + child: _OptionCheckBox( + context, + "Use texture rendering", + kOptionTextureRender, + optGetter: bind.mainGetUseTextureRender, + optSetter: (k, v) async => + await bind.mainSetLocalOption(key: k, value: v ? 'Y' : 'N'), + ), ), - ), - if (!bind.isCustomClient()) + if (!isWeb && !bind.isCustomClient()) _OptionCheckBox( context, 'Check for software update on startup', @@ -443,7 +465,7 @@ class _GeneralState extends State<_General> { ) ], ]; - if (bind.mainShowOption(key: kOptionAllowLinuxHeadless)) { + if (!isWeb && bind.mainShowOption(key: kOptionAllowLinuxHeadless)) { children.add(_OptionCheckBox( context, 'Allow linux headless', kOptionAllowLinuxHeadless)); } @@ -641,8 +663,9 @@ class _GeneralState extends State<_General> { initialKey: currentKey, onChanged: (key) async { await bind.mainSetLocalOption(key: kCommConfKeyLang, value: key); - reloadAllWindows(); - bind.mainChangeLanguage(lang: key); + if (isWeb) reloadCurrentWindow(); + if (!isWeb) reloadAllWindows(); + if (!isWeb) bind.mainChangeLanguage(lang: key); }, enabled: !isOptFixed, ).marginOnly(left: _kContentHMargin); @@ -1337,7 +1360,7 @@ class _Network extends StatefulWidget { class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin { @override bool get wantKeepAlive => true; - bool locked = bind.mainIsInstalled(); + bool locked = !isWeb && bind.mainIsInstalled(); @override Widget build(BuildContext context) { @@ -1346,8 +1369,9 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin { final scrollController = ScrollController(); final hideServer = bind.mainGetBuildinOption(key: kOptionHideServerSetting) == 'Y'; + // TODO: support web proxy final hideProxy = - bind.mainGetBuildinOption(key: kOptionHideProxySetting) == 'Y'; + isWeb || bind.mainGetBuildinOption(key: kOptionHideProxySetting) == 'Y'; return DesktopScrollWrapper( scrollController: scrollController, child: ListView( @@ -1467,7 +1491,7 @@ class _DisplayState extends State<_Display> { scrollStyle(context), imageQuality(context), codec(context), - privacyModeImpl(context), + if (!isWeb) privacyModeImpl(context), other(context), ]).marginOnly(bottom: _kListViewBottomMargin)); } diff --git a/flutter/lib/mobile/pages/connection_page.dart b/flutter/lib/mobile/pages/connection_page.dart index 02c552d716d7..9fcef8e3f14b 100644 --- a/flutter/lib/mobile/pages/connection_page.dart +++ b/flutter/lib/mobile/pages/connection_page.dart @@ -9,19 +9,16 @@ import 'package:url_launcher/url_launcher.dart'; import 'package:flutter_hbb/models/peer_model.dart'; import '../../common.dart'; -import '../../common/widgets/login.dart'; import '../../common/widgets/peer_tab_page.dart'; import '../../common/widgets/autocomplete.dart'; import '../../consts.dart'; import '../../models/model.dart'; import '../../models/platform_model.dart'; import 'home_page.dart'; -import 'scan_page.dart'; -import 'settings_page.dart'; /// Connection page for connecting to a remote peer. class ConnectionPage extends StatefulWidget implements PageShape { - ConnectionPage({Key? key}) : super(key: key); + ConnectionPage({Key? key, required this.appBarActions}) : super(key: key); @override final icon = const Icon(Icons.connected_tv); @@ -30,7 +27,7 @@ class ConnectionPage extends StatefulWidget implements PageShape { final title = translate("Connection"); @override - final appBarActions = isWeb ? [const WebMenu()] : []; + final List appBarActions; @override State createState() => _ConnectionPageState(); @@ -356,73 +353,3 @@ class _ConnectionPageState extends State { super.dispose(); } } - -class WebMenu extends StatefulWidget { - const WebMenu({Key? key}) : super(key: key); - - @override - State createState() => _WebMenuState(); -} - -class _WebMenuState extends State { - @override - Widget build(BuildContext context) { - Provider.of(context); - return PopupMenuButton( - tooltip: "", - icon: const Icon(Icons.more_vert), - itemBuilder: (context) { - return (isIOS - ? [ - const PopupMenuItem( - value: "scan", - child: Icon(Icons.qr_code_scanner, color: Colors.black), - ) - ] - : >[]) + - [ - PopupMenuItem( - value: "server", - child: Text(translate('ID/Relay Server')), - ) - ] + - [ - PopupMenuItem( - value: "login", - child: Text(gFFI.userModel.userName.value.isEmpty - ? translate("Login") - : '${translate("Logout")} (${gFFI.userModel.userName.value})'), - ) - ] + - [ - PopupMenuItem( - value: "about", - child: Text(translate('About RustDesk')), - ) - ]; - }, - onSelected: (value) { - if (value == 'server') { - showServerSettings(gFFI.dialogManager); - } - if (value == 'about') { - showAbout(gFFI.dialogManager); - } - if (value == 'login') { - if (gFFI.userModel.userName.value.isEmpty) { - loginDialog(); - } else { - logOutConfirmDialog(); - } - } - if (value == 'scan') { - Navigator.push( - context, - MaterialPageRoute( - builder: (BuildContext context) => ScanPage(), - ), - ); - } - }); - } -} diff --git a/flutter/lib/mobile/pages/home_page.dart b/flutter/lib/mobile/pages/home_page.dart index 078e2b2f7b3c..0db7a2b91e3a 100644 --- a/flutter/lib/mobile/pages/home_page.dart +++ b/flutter/lib/mobile/pages/home_page.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_hbb/mobile/pages/server_page.dart'; import 'package:flutter_hbb/mobile/pages/settings_page.dart'; +import 'package:flutter_hbb/web/settings_page.dart'; import 'package:get/get.dart'; import '../../common.dart'; import '../../common/widgets/chat_page.dart'; @@ -45,7 +46,10 @@ class HomePageState extends State { void initPages() { _pages.clear(); - if (!bind.isIncomingOnly()) _pages.add(ConnectionPage()); + if (!bind.isIncomingOnly()) + _pages.add(ConnectionPage( + appBarActions: [], + )); if (isAndroid && !bind.isOutgoingOnly()) { _chatPageTabIndex = _pages.length; _pages.addAll([ChatPage(type: ChatPageType.mobileMain), ServerPage()]); @@ -149,7 +153,8 @@ class HomePageState extends State { } class WebHomePage extends StatelessWidget { - final connectionPage = ConnectionPage(); + final connectionPage = + ConnectionPage(appBarActions: [const WebSettingsPage()]); @override Widget build(BuildContext context) { diff --git a/flutter/lib/web/bridge.dart b/flutter/lib/web/bridge.dart index 3aa65a5be69f..305ed0b7550b 100644 --- a/flutter/lib/web/bridge.dart +++ b/flutter/lib/web/bridge.dart @@ -736,7 +736,8 @@ class RustdeskImpl { } Future mainGetLicense({dynamic hint}) { - throw UnimplementedError(); + // TODO: implement + return Future(() => ''); } Future mainGetVersion({dynamic hint}) { @@ -975,10 +976,11 @@ class RustdeskImpl { Future mainSetUserDefaultOption( {required String key, required String value, dynamic hint}) { - return js.context.callMethod('getByName', [ + js.context.callMethod('setByName', [ 'option:user:default', jsonEncode({'name': key, 'value': value}) ]); + return Future.value(); } String mainGetUserDefaultOption({required String key, dynamic hint}) { @@ -1052,7 +1054,7 @@ class RustdeskImpl { } Future mainGetLangs({dynamic hint}) { - throw UnimplementedError(); + return Future(() => js.context.callMethod('getByName', ['langs'])); } Future mainGetTemporaryPassword({dynamic hint}) { @@ -1064,7 +1066,8 @@ class RustdeskImpl { } Future mainGetFingerprint({dynamic hint}) { - throw UnimplementedError(); + // TODO: implement + return Future.value(''); } Future cmGetClientsState({dynamic hint}) { @@ -1106,7 +1109,7 @@ class RustdeskImpl { } String mainSupportedHwdecodings({dynamic hint}) { - throw UnimplementedError(); + return '{}'; } Future mainIsRoot({dynamic hint}) { @@ -1295,8 +1298,7 @@ class RustdeskImpl { } Future mainGetBuildDate({dynamic hint}) { - // TODO - throw UnimplementedError(); + return Future(() => js.context.callMethod('getByName', ['build_date'])); } String translate( diff --git a/flutter/lib/web/settings_page.dart b/flutter/lib/web/settings_page.dart new file mode 100644 index 000000000000..13ba6cb2f275 --- /dev/null +++ b/flutter/lib/web/settings_page.dart @@ -0,0 +1,98 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hbb/desktop/pages/desktop_setting_page.dart'; +import 'package:flutter_hbb/mobile/pages/scan_page.dart'; +import 'package:flutter_hbb/mobile/pages/settings_page.dart'; +import 'package:provider/provider.dart'; + +import '../../common.dart'; +import '../../common/widgets/login.dart'; +import '../../models/model.dart'; + +class WebSettingsPage extends StatelessWidget { + const WebSettingsPage({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + if (isWebDesktop) { + return _buildDesktopButton(context); + } else { + return _buildMobileMenu(context); + } + } + + Widget _buildDesktopButton(BuildContext context) { + return IconButton( + icon: const Icon(Icons.more_vert), + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (BuildContext context) => + DesktopSettingPage(initialTabkey: SettingsTabKey.general), + ), + ); + }, + ); + } + + Widget _buildMobileMenu(BuildContext context) { + Provider.of(context); + return PopupMenuButton( + tooltip: "", + icon: const Icon(Icons.more_vert), + itemBuilder: (context) { + return (isIOS + ? [ + const PopupMenuItem( + value: "scan", + child: Icon(Icons.qr_code_scanner, color: Colors.black), + ) + ] + : >[]) + + [ + PopupMenuItem( + value: "server", + child: Text(translate('ID/Relay Server')), + ) + ] + + [ + PopupMenuItem( + value: "login", + child: Text(gFFI.userModel.userName.value.isEmpty + ? translate("Login") + : '${translate("Logout")} (${gFFI.userModel.userName.value})'), + ) + ] + + [ + PopupMenuItem( + value: "about", + child: Text(translate('About RustDesk')), + ) + ]; + }, + onSelected: (value) { + if (value == 'server') { + showServerSettings(gFFI.dialogManager); + } + if (value == 'about') { + showAbout(gFFI.dialogManager); + } + if (value == 'login') { + if (gFFI.userModel.userName.value.isEmpty) { + loginDialog(); + } else { + logOutConfirmDialog(); + } + } + if (value == 'scan') { + Navigator.push( + context, + MaterialPageRoute( + builder: (BuildContext context) => ScanPage(), + ), + ); + } + }); + } +} + From 532fe6aefbecffc4bfc9799f67839f8b7dc55e9f Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Sun, 1 Sep 2024 23:14:57 +0800 Subject: [PATCH 150/541] refact: web ui, login (#9225) Signed-off-by: fufesou --- flutter/lib/web/bridge.dart | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/flutter/lib/web/bridge.dart b/flutter/lib/web/bridge.dart index 305ed0b7550b..d4b09430aa1f 100644 --- a/flutter/lib/web/bridge.dart +++ b/flutter/lib/web/bridge.dart @@ -4,6 +4,7 @@ import 'dart:convert'; import 'dart:typed_data'; import 'package:flutter/foundation.dart'; import 'package:uuid/uuid.dart'; +import 'dart:html' as html; import 'package:flutter_hbb/consts.dart'; @@ -655,7 +656,15 @@ class RustdeskImpl { } String mainGetLoginDeviceInfo({dynamic hint}) { - throw UnimplementedError(); + String userAgent = html.window.navigator.userAgent; + String appName = html.window.navigator.appName; + String appVersion = html.window.navigator.appVersion; + String? platform = html.window.navigator.platform; + return jsonEncode({ + 'os': '$userAgent, $appName $appVersion ($platform)', + 'type': 'Web client', + 'name': js.context.callMethod('getByName', ['my_name']), + }); } Future mainChangeId({required String newId, dynamic hint}) { @@ -850,11 +859,11 @@ class RustdeskImpl { } Future mainGetMyId({dynamic hint}) { - throw UnimplementedError(); + return Future(() => js.context.callMethod('getByName', ['my_id'])); } Future mainGetUuid({dynamic hint}) { - throw UnimplementedError(); + return Future(() => js.context.callMethod('getByName', ['uuid'])); } Future mainGetPeerOption( @@ -1066,8 +1075,7 @@ class RustdeskImpl { } Future mainGetFingerprint({dynamic hint}) { - // TODO: implement - return Future.value(''); + return Future(() => js.context.callMethod('getByName', ['fingerprint'])); } Future cmGetClientsState({dynamic hint}) { From 827efabbc00403654e914e4f4663951e5ca06b25 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Mon, 2 Sep 2024 02:00:59 +0800 Subject: [PATCH 151/541] refact: remove fingerprint for web (#9226) Signed-off-by: fufesou --- flutter/lib/desktop/pages/desktop_setting_page.dart | 7 ++++--- flutter/lib/web/bridge.dart | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 4ab1a8c56671..0e5fac4caf58 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -1902,9 +1902,10 @@ class _AboutState extends State<_About> { SelectionArea( child: Text('${translate('Build Date')}: $buildDate') .marginSymmetric(vertical: 4.0)), - SelectionArea( - child: Text('${translate('Fingerprint')}: $fingerprint') - .marginSymmetric(vertical: 4.0)), + if (!isWeb) + SelectionArea( + child: Text('${translate('Fingerprint')}: $fingerprint') + .marginSymmetric(vertical: 4.0)), InkWell( onTap: () { launchUrlString('https://rustdesk.com/privacy.html'); diff --git a/flutter/lib/web/bridge.dart b/flutter/lib/web/bridge.dart index d4b09430aa1f..8805831bc53b 100644 --- a/flutter/lib/web/bridge.dart +++ b/flutter/lib/web/bridge.dart @@ -1075,7 +1075,7 @@ class RustdeskImpl { } Future mainGetFingerprint({dynamic hint}) { - return Future(() => js.context.callMethod('getByName', ['fingerprint'])); + return Future.value(''); } Future cmGetClientsState({dynamic hint}) { From 75a4671bda78d0adba0d347cb2081bc650161647 Mon Sep 17 00:00:00 2001 From: SimonHanel <43139362+SimonHanel@users.noreply.github.com> Date: Tue, 3 Sep 2024 04:09:25 +0200 Subject: [PATCH 152/541] Update da.rs (#9238) * Update da.rs Filled out every empty string and adjusted some for better translation. Some translations might be janky due to my lack of context for what the string is used for, but it's good enough for now. * Update da.rs Minor tweaks --- src/lang/da.rs | 442 ++++++++++++++++++++++++------------------------- 1 file changed, 221 insertions(+), 221 deletions(-) diff --git a/src/lang/da.rs b/src/lang/da.rs index a9e286600b66..03cf47d4bb8d 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -3,7 +3,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = [ ("Status", "Status"), ("Your Desktop", "Dit skrivebord"), - ("desk_tip", "Du kan få adgang til dit skrivebord med dette ID og adgangskode."), + ("desk_tip", "Du kan give adgang til dit skrivebord med dette ID og denne adgangskode."), ("Password", "Adgangskode"), ("Ready", "Klar"), ("Established", "Etableret"), @@ -38,18 +38,18 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop service", "Sluk for forbindelsesserveren"), ("Change ID", "Ændr ID"), ("Your new ID", "Dit nye ID"), - ("length %min% to %max%", ""), - ("starts with a letter", ""), - ("allowed characters", ""), - ("id_change_tip", "Kun tegnene a-z, A-Z, 0-9 og _ (understregning) er tilladt. Det første bogstav skal være a-z, A-Z. Længde mellem 6 og 16."), + ("length %min% to %max%", "længde %min% til %max%"), + ("starts with a letter", "starter med ét bogstav"), + ("allowed characters", "tilladte tegn"), + ("id_change_tip", "Kun tegnene a-z, A-Z, 0-9 og _ (understregning) er tilladt. Det første bogstav skal være a-z, A-Z. Antal tegn skal være mellem 6 og 16."), ("Website", "Hjemmeside"), ("About", "Om"), - ("Slogan_tip", ""), - ("Privacy Statement", ""), + ("Slogan_tip", "Lavet med kærlighed i denne kaotiske verden!"), + ("Privacy Statement", "Privatlivspolitik"), ("Mute", "Sluk for mikrofonen"), - ("Build Date", ""), - ("Version", ""), - ("Home", ""), + ("Build Date", "Build dato"), + ("Version", "Version"), + ("Home", "Hjem"), ("Audio Input", "Lydinput"), ("Enhancements", "Forbedringer"), ("Hardware Codec", "Hardware-codec"), @@ -120,8 +120,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Original", "Original"), ("Shrink", "Krymp"), ("Stretch", "Stræk ud"), - ("Scrollbar", "Rullebar"), - ("ScrollAuto", "Auto-rul"), + ("Scrollbar", "Scrollbar"), + ("ScrollAuto", "Auto-scroll"), ("Good image quality", "God billedkvalitet"), ("Balanced", "Afbalanceret"), ("Optimize reaction time", "Optimeret responstid"), @@ -139,9 +139,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Remote desktop is offline", "Fjernskrivebord er offline"), ("Key mismatch", "Nøgle uoverensstemmelse"), ("Timeout", "Timeout"), - ("Failed to connect to relay server", "Forbindelse til relæ-serveren mislykkedes"), + ("Failed to connect to relay server", "Forbindelse til relay-serveren mislykkedes"), ("Failed to connect via rendezvous server", "Forbindelse via Rendezvous-server mislykkedes"), - ("Failed to connect via relay server", "Forbindelse via relæ-serveren mislykkedes"), + ("Failed to connect via relay server", "Forbindelse via relay-serveren mislykkedes"), ("Failed to make direct connection to remote desktop", "Direkte forbindelse til fjernskrivebord kunne ikke etableres"), ("Set Password", "Indstil adgangskode"), ("OS Password", "Operativsystemadgangskode"), @@ -218,7 +218,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Remember me", "Husk mig"), ("Trust this device", "Husk denne enhed"), ("Verification code", "Verifikationskode"), - ("verification_tip", ""), + ("verification_tip", "En bekræftelseskode er blevet sendt til den registrerede e-mail adresse. Indtast bekræftelseskoden for at logge på."), ("Logout", "Logger af"), ("Tags", "Nøgleord"), ("Search ID", "Søg efter ID"), @@ -230,7 +230,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Username missed", "Glemt brugernavn"), ("Password missed", "Glemt kodeord"), ("Wrong credentials", "Forkerte registreringsdata"), - ("The verification code is incorrect or has expired", ""), + ("The verification code is incorrect or has expired", "Bekræftelsesnøglen er forkert eller er udløbet"), ("Edit Tag", "Rediger nøgleord"), ("Forget Password", "Glem adgangskoden"), ("Favorites", "Favoritter"), @@ -248,7 +248,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Are you sure to close the connection?", "Er du sikker på at du vil afslutte forbindelsen?"), ("Download new version", "Download ny version"), ("Touch mode", "Touch-tilstand"), - ("Mouse mode", "Musse-tilstand"), + ("Mouse mode", "Muse-tilstand"), ("One-Finger Tap", "En-finger-tryk"), ("Left Mouse", "Venstre mus"), ("One-Long Tap", "Tryk og hold med en finger"), @@ -286,8 +286,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("android_service_will_start_tip", "Ved at tænde for skærmoptagelsen startes tjenesten automatisk, så andre enheder kan anmode om en forbindelse fra denne enhed."), ("android_stop_service_tip", "Ved at lukke tjenesten lukkes alle fremstillede forbindelser automatisk."), ("android_version_audio_tip", "Den aktuelle Android-version understøtter ikke lydoptagelse. Android 10 eller højere er påkrævet."), - ("android_start_service_tip", ""), - ("android_permission_may_not_change_tip", ""), + ("android_start_service_tip", "Tryk [Start tjeneste] eller aktivér [Skærmoptagelse] tilladelse for at dele skærmen."), + ("android_permission_may_not_change_tip", "Rettigheder til oprettede forbindelser ændres ikke med det samme før der forbindelsen genoprettes."), ("Account", "Konto"), ("Overwrite", "Overskriv"), ("This file exists, skip or overwrite this file?", "Denne fil findes allerede, vil du springe over eller overskrive denne fil?"), @@ -305,7 +305,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Language", "Sprog"), ("Keep RustDesk background service", "Behold RustDesk baggrundstjeneste"), ("Ignore Battery Optimizations", "Ignorér betteri optimeringer"), - ("android_open_battery_optimizations_tip", ""), + ("android_open_battery_optimizations_tip", "Hvis du ønsker at slukke for denne funktion, åbn RustDesk appens indstillinger, tryk på [Batteri], og fjern flueben ved [Uden begrænsninger]"), ("Start on boot", "Start under opstart"), ("Start the screen sharing service on boot, requires special permissions", "Start skærmdelingstjenesten under opstart, kræver specielle rettigheder"), ("Connection not allowed", "Forbindelse ikke tilladt"), @@ -313,7 +313,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Map mode", "Kortmodus"), ("Translate mode", "Oversættelsesmodus"), ("Use permanent password", "Brug permanent adgangskode"), - ("Use both passwords", "Brug begge adgangskoder"), + ("Use both passwords", "Brug begge typer adgangskoder"), ("Set permanent password", "Sæt permanent adgangskode"), ("Enable remote restart", "Aktivér fjerngenstart"), ("Restart remote device", "Genstart fjernenhed"), @@ -330,8 +330,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Ratio", "Forhold"), ("Image Quality", "Billedkvalitet"), ("Scroll Style", "Rullestil"), - ("Show Toolbar", ""), - ("Hide Toolbar", ""), + ("Show Toolbar", "Vis værktøjslinje"), + ("Hide Toolbar", "Skjul værktøjslinje"), ("Direct Connection", "Direkte forbindelse"), ("Relay Connection", "Viderestillingsforbindelse"), ("Secure Connection", "Sikker forbindelse"), @@ -359,8 +359,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Audio Input Device", "Lydindgangsenhed"), ("Use IP Whitelisting", "Brug IP Whitelisting"), ("Network", "Netværk"), - ("Pin Toolbar", ""), - ("Unpin Toolbar", ""), + ("Pin Toolbar", "Fastgør værktøjslinjen"), + ("Unpin Toolbar", "Frigiv værktøjslinjen"), ("Recording", "Optager"), ("Directory", "Mappe"), ("Automatically record incoming sessions", "Optag automatisk indgående sessioner"), @@ -368,15 +368,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Start session recording", "Start sessionsoptagelse"), ("Stop session recording", "Stop sessionsoptagelse"), ("Enable recording session", "Aktivér optagelsessession"), - ("Enable LAN discovery", "Aktivér LAN Discovery"), - ("Deny LAN discovery", "Afvis LAN Discovery"), + ("Enable LAN discovery", "Aktivér opdagelse via det lokale netværk"), + ("Deny LAN discovery", "Afvis opdagelse via det lokale netværk"), ("Write a message", "Skriv en besked"), ("Prompt", "Prompt"), ("Please wait for confirmation of UAC...", "Vent venligst på UAC-bekræftelse..."), - ("elevated_foreground_window_tip", ""), + ("elevated_foreground_window_tip", "Det nuværende vindue på fjernskrivebordet kræver højere rettigheder for at køre, så det er midlertidigt ikke muligt at bruge musen og tastaturet. Du kan bede fjernbrugeren om at minimere vinduet, eller trykke på elevér knappen i forbindelsesvinduet. For at undgå dette problem, er det anbefalet at installere RustDesk på fjernenheden."), ("Disconnected", "Afbrudt"), ("Other", "Andre"), - ("Confirm before closing multiple tabs", "Bekræft før du lukker flere faner"), + ("Confirm before closing multiple tabs", "Bekræft nedlukning hvis der er flere faner"), ("Keyboard Settings", "Tastaturindstillinger"), ("Full Access", "Fuld adgang"), ("Screen Share", "Skærmdeling"), @@ -399,8 +399,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("One-time password length", "Engangskode længde"), ("Request access to your device", "Efterspørg adgang til din enhed"), ("Hide connection management window", "Skjul forbindelseshåndteringsvindue"), - ("hide_cm_tip", ""), - ("wayland_experiment_tip", ""), + ("hide_cm_tip", "Tillad at skjule, hvis der kun forbindes ved brug af midlertidige og permanente adgangskoder"), + ("wayland_experiment_tip", "Wayland understøttelse er stadigvæk under udvikling. Hvis du har brug for ubemandet adgang, bedes du bruge X11."), ("Right click to select tabs", "Højreklik for at vælge faner"), ("Skipped", "Sprunget over"), ("Add to address book", "Tilføj til adressebog"), @@ -409,19 +409,19 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by web console", "Lukket ned manuelt af webkonsollen"), ("Local keyboard type", "Lokal tastatur type"), ("Select local keyboard type", "Vælg lokal tastatur type"), - ("software_render_tip", ""), + ("software_render_tip", "Hvis du bruger et Nvidia grafikkort på Linux, og fjernskrivebordsvinduet lukker ned med det samme efter forbindelsen er oprettet, kan det hjælpe at skifte til Nouveau open-source driveren, og aktivere software rendering. Et genstart af RustDesk er nødvendigt."), ("Always use software rendering", "Brug altid software rendering"), - ("config_input", ""), - ("config_microphone", ""), - ("request_elevation_tip", ""), + ("config_input", "For at styre fjernskrivebordet med tastaturet, skal du give Rustdesk rettigheder til at optage tastetryk"), + ("config_microphone", "For at tale sammen over fjernstyring, skal du give RustDesk rettigheder til at optage lyd"), + ("request_elevation_tip", "Du kan også spørge om elevationsrettigheder, hvis der er nogen i nærheden af fjernenheden."), ("Wait", "Vent"), ("Elevation Error", "Elevationsfejl"), ("Ask the remote user for authentication", "Spørg fjernbrugeren for godkendelse"), ("Choose this if the remote account is administrator", "Vælg dette hvis fjernbrugeren er en administrator"), ("Transmit the username and password of administrator", "Send brugernavnet og adgangskoden på administratoren"), - ("still_click_uac_tip", ""), + ("still_click_uac_tip", "Kræver stadigvæk at fjernbrugeren skal trykke OK på UAC vinduet ved kørsel af RustDesk."), ("Request Elevation", "Efterspørger elevation"), - ("wait_accept_uac_tip", ""), + ("wait_accept_uac_tip", "Vent venligst på at fjernbrugeren accepterer UAC dialog forespørgslen."), ("Elevate successfully", "Elevation lykkedes"), ("uppercase", "store bogstaver"), ("lowercase", "små bogstaver"), @@ -435,7 +435,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please confirm if you want to share your desktop?", "Bekræft venligst, om du vil dele dit skrivebord?"), ("Display", "Visning"), ("Default View Style", "Standard visningsstil"), - ("Default Scroll Style", "Standard rulle stil"), + ("Default Scroll Style", "Standard scrollestil"), ("Default Image Quality", "Standard billedkvalitet"), ("Default Codec", "Standard codec"), ("Bitrate", "Bitrate"), @@ -445,7 +445,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", "Stemmeopkald"), ("Text chat", "Tekstchat"), ("Stop voice call", "Stop stemmeopkald"), - ("relay_hint_tip", ""), + ("relay_hint_tip", "Det kan ske, at det ikke er muligt at forbinde direkte; du kan forsøge at forbinde via en relay-server. Derudover, hvis du ønsker at bruge en relay-server på dit første forsøg, kan du tilføje \"/r\" efter ID'et, eller bruge valgmuligheden \"Forbind altid via relay-server\" i fanen for seneste sessioner, hvis den findes."), ("Reconnect", "Genopret"), ("Codec", "Codec"), ("Resolution", "Opløsning"), @@ -458,191 +458,191 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Minimize", "Minimér"), ("Maximize", "Maksimér"), ("Your Device", "Din enhed"), - ("empty_recent_tip", ""), - ("empty_favorite_tip", ""), - ("empty_lan_tip", ""), - ("empty_address_book_tip", ""), + ("empty_recent_tip", "Ups, ingen seneste sessioner!\nTid til at oprette en ny."), + ("empty_favorite_tip", "Ingen yndlings modparter endnu?\nLad os finde én at forbinde til, og tilføje den til dine favoritter!"), + ("empty_lan_tip", "Åh nej, det ser ud til, at vi ikke kunne finde nogen modparter endnu."), + ("empty_address_book_tip", "Åh nej, det ser ud til at der ikke er nogle modparter der er tilføjet til din adressebog."), ("eg: admin", "fx: admin"), ("Empty Username", "Tom brugernavn"), ("Empty Password", "Tom adgangskode"), ("Me", "Mig"), - ("identical_file_tip", ""), - ("show_monitors_tip", ""), - ("View Mode", ""), - ("login_linux_tip", ""), - ("verify_rustdesk_password_tip", ""), - ("remember_account_tip", ""), - ("os_account_desk_tip", ""), - ("OS Account", ""), - ("another_user_login_title_tip", ""), - ("another_user_login_text_tip", ""), - ("xorg_not_found_title_tip", ""), - ("xorg_not_found_text_tip", ""), - ("no_desktop_title_tip", ""), - ("no_desktop_text_tip", ""), - ("No need to elevate", ""), - ("System Sound", ""), - ("Default", ""), - ("New RDP", ""), - ("Fingerprint", ""), - ("Copy Fingerprint", ""), - ("no fingerprints", ""), - ("Select a peer", ""), - ("Select peers", ""), - ("Plugins", ""), - ("Uninstall", ""), - ("Update", ""), - ("Enable", ""), - ("Disable", ""), - ("Options", ""), - ("resolution_original_tip", ""), - ("resolution_fit_local_tip", ""), - ("resolution_custom_tip", ""), - ("Collapse toolbar", ""), - ("Accept and Elevate", ""), - ("accept_and_elevate_btn_tooltip", ""), - ("clipboard_wait_response_timeout_tip", ""), - ("Incoming connection", ""), - ("Outgoing connection", ""), - ("Exit", ""), - ("Open", ""), - ("logout_tip", ""), - ("Service", ""), - ("Start", ""), - ("Stop", ""), - ("exceed_max_devices", ""), - ("Sync with recent sessions", ""), - ("Sort tags", ""), - ("Open connection in new tab", ""), - ("Move tab to new window", ""), - ("Can not be empty", ""), - ("Already exists", ""), - ("Change Password", ""), - ("Refresh Password", ""), - ("ID", ""), - ("Grid View", ""), - ("List View", ""), - ("Select", ""), - ("Toggle Tags", ""), - ("pull_ab_failed_tip", ""), - ("push_ab_failed_tip", ""), - ("synced_peer_readded_tip", ""), - ("Change Color", ""), - ("Primary Color", ""), - ("HSV Color", ""), - ("Installation Successful!", ""), - ("Installation failed!", ""), - ("Reverse mouse wheel", ""), - ("{} sessions", ""), - ("scam_title", ""), - ("scam_text1", ""), - ("scam_text2", ""), - ("Don't show again", ""), - ("I Agree", ""), - ("Decline", ""), - ("Timeout in minutes", ""), - ("auto_disconnect_option_tip", ""), - ("Connection failed due to inactivity", ""), - ("Check for software update on startup", ""), - ("upgrade_rustdesk_server_pro_to_{}_tip", ""), - ("pull_group_failed_tip", ""), - ("Filter by intersection", ""), - ("Remove wallpaper during incoming sessions", ""), - ("Test", ""), - ("display_is_plugged_out_msg", ""), - ("No displays", ""), - ("Open in new window", ""), - ("Show displays as individual windows", ""), - ("Use all my displays for the remote session", ""), - ("selinux_tip", ""), - ("Change view", ""), - ("Big tiles", ""), - ("Small tiles", ""), - ("List", ""), - ("Virtual display", ""), - ("Plug out all", ""), - ("True color (4:4:4)", ""), - ("Enable blocking user input", ""), - ("id_input_tip", ""), - ("privacy_mode_impl_mag_tip", ""), - ("privacy_mode_impl_virtual_display_tip", ""), - ("Enter privacy mode", ""), - ("Exit privacy mode", ""), - ("idd_not_support_under_win10_2004_tip", ""), - ("input_source_1_tip", ""), - ("input_source_2_tip", ""), - ("Swap control-command key", ""), - ("swap-left-right-mouse", ""), - ("2FA code", ""), - ("More", ""), - ("enable-2fa-title", ""), - ("enable-2fa-desc", ""), - ("wrong-2fa-code", ""), - ("enter-2fa-title", ""), - ("Email verification code must be 6 characters.", ""), - ("2FA code must be 6 digits.", ""), - ("Multiple Windows sessions found", ""), - ("Please select the session you want to connect to", ""), - ("powered_by_me", ""), - ("outgoing_only_desk_tip", ""), - ("preset_password_warning", ""), - ("Security Alert", ""), - ("My address book", ""), - ("Personal", ""), - ("Owner", ""), - ("Set shared password", ""), - ("Exist in", ""), - ("Read-only", ""), - ("Read/Write", ""), - ("Full Control", ""), - ("share_warning_tip", ""), - ("Everyone", ""), - ("ab_web_console_tip", ""), - ("allow-only-conn-window-open-tip", ""), - ("no_need_privacy_mode_no_physical_displays_tip", ""), - ("Follow remote cursor", ""), - ("Follow remote window focus", ""), - ("default_proxy_tip", ""), - ("no_audio_input_device_tip", ""), - ("Incoming", ""), - ("Outgoing", ""), - ("Clear Wayland screen selection", ""), - ("clear_Wayland_screen_selection_tip", ""), - ("confirm_clear_Wayland_screen_selection_tip", ""), - ("android_new_voice_call_tip", ""), - ("texture_render_tip", ""), - ("Use texture rendering", ""), - ("Floating window", ""), - ("floating_window_tip", ""), - ("Keep screen on", ""), - ("Never", ""), - ("During controlled", ""), - ("During service is on", ""), - ("Capture screen using DirectX", ""), - ("Back", ""), - ("Apps", ""), - ("Volume up", ""), - ("Volume down", ""), - ("Power", ""), - ("Telegram bot", ""), - ("enable-bot-tip", ""), - ("enable-bot-desc", ""), - ("cancel-2fa-confirm-tip", ""), - ("cancel-bot-confirm-tip", ""), - ("About RustDesk", ""), - ("Send clipboard keystrokes", ""), - ("network_error_tip", ""), - ("Unlock with PIN", ""), - ("Requires at least {} characters", ""), - ("Wrong PIN", ""), - ("Set PIN", ""), - ("Enable trusted devices", ""), - ("Manage trusted devices", ""), - ("Platform", ""), - ("Days remaining", ""), - ("enable-trusted-devices-tip", ""), - ("Parent directory", ""), - ("Resume", ""), - ("Invalid file name", ""), + ("identical_file_tip", "Denne fil er identisk med modpartens."), + ("show_monitors_tip", "Vis skærme i værktøjsbjælken"), + ("View Mode", "Visningstilstand"), + ("login_linux_tip", "Du skal logge på en fjernstyret Linux konto for at aktivere en X skrivebordssession"), + ("verify_rustdesk_password_tip", "Bekræft RustDesk adgangskode"), + ("remember_account_tip", "Husk denne konto"), + ("os_account_desk_tip", "Denne konto benyttes til at logge på fjernsystemet, og aktivere skrivebordssessionen i hovedløs tilstand"), + ("OS Account", "Styresystem konto"), + ("another_user_login_title_tip", "En anden bruger er allerede logget ind"), + ("another_user_login_text_tip", "Frakobl"), + ("xorg_not_found_title_tip", "Xorg ikke fundet"), + ("xorg_not_found_text_tip", "Installér venlist Xorg"), + ("no_desktop_title_tip", "Intet skrivebordsmiljø er tilgængeligt"), + ("no_desktop_text_tip", "Installér venligst GNOME skrivebordet"), + ("No need to elevate", "Ingen grund til at elevere"), + ("System Sound", "Systemlyd"), + ("Default", "Standard"), + ("New RDP", "Ny RDP"), + ("Fingerprint", "Fingeraftryk"), + ("Copy Fingerprint", "Kopiér fingeraftryk"), + ("no fingerprints", "Ingen fingeraftryk"), + ("Select a peer", "Vælg en peer"), + ("Select peers", "Vælg peers"), + ("Plugins", "Plugins"), + ("Uninstall", "Afinstallér"), + ("Update", "Opdatér"), + ("Enable", "Aktivér"), + ("Disable", "Deaktivér"), + ("Options", "Valgmuligheder"), + ("resolution_original_tip", "Original skærmopløsning"), + ("resolution_fit_local_tip", "Tilpas lokal skærmopløsning"), + ("resolution_custom_tip", "Bruger-tilpasset skærmopløsning"), + ("Collapse toolbar", "Skjul værktøjsbjælke"), + ("Accept and Elevate", "Acceptér og elevér"), + ("accept_and_elevate_btn_tooltip", "Acceptér forbindelsen og elevér UAC tilladelser"), + ("clipboard_wait_response_timeout_tip", "Tiden for at vente på en kopieringsforespørgsel udløb"), + ("Incoming connection", "Indgående forbindelse"), + ("Outgoing connection", "Udgående forbindelse"), + ("Exit", "Afslut"), + ("Open", "Åben"), + ("logout_tip", "Er du sikker på at du vil logge af?"), + ("Service", "Tjeneste"), + ("Start", "Start"), + ("Stop", "Stop"), + ("exceed_max_devices", "Du har nået det maksimale antal håndtérbare enheder."), + ("Sync with recent sessions", "Synkronisér med tidligere sessioner"), + ("Sort tags", "Sortér nøgleord"), + ("Open connection in new tab", "Åbn forbindelse i en ny fane"), + ("Move tab to new window", "Flyt fane i et nyt vindue"), + ("Can not be empty", "Kan ikke være tom"), + ("Already exists", "Findes allerede"), + ("Change Password", "Skift adgangskode"), + ("Refresh Password", "Genopfrisk adgangskode"), + ("ID", "ID"), + ("Grid View", "Gittervisning"), + ("List View", "Listevisning"), + ("Select", "Vælg"), + ("Toggle Tags", "Slå nøgleord til/fra"), + ("pull_ab_failed_tip", "Opdatering af adressebog mislykkedes"), + ("push_ab_failed_tip", "Synkronisering af adressebog til serveren mislykkedes"), + ("synced_peer_readded_tip", "Enhederne, som var til stede i de seneste sessioner, vil blive synkroniseret tilbage til adressebogen."), + ("Change Color", "Skift farve"), + ("Primary Color", "Primær farve"), + ("HSV Color", "HSV farve"), + ("Installation Successful!", "Installation fuldført!"), + ("Installation failed!", "Installation mislykkedes!"), + ("Reverse mouse wheel", "Invertér musehjul"), + ("{} sessions", "{} sessioner"), + ("scam_title", "ADVARSEL: Du kan blive SVINDLET!"), + ("scam_text1", "Hvis du taler telefon med en person du IKKE kender, og IKKE stoler på, som har bedt dig om at bruge RustDesk til at forbinde til din PC, stop med det samme, og læg på omgående."), + ("scam_text2", "Det er højest sandsynligvis en svinder som forsøger at stjæle dine penge eller andre personlige oplysninger."), + ("Don't show again", "Vis ikke igen"), + ("I Agree", "Jeg accepterer"), + ("Decline", "Afvis"), + ("Timeout in minutes", "Udløbstid i minutter"), + ("auto_disconnect_option_tip", "Luk automatisk indkommende sessioner ved inaktivitet"), + ("Connection failed due to inactivity", "Forbindelsen blev afbrudt grundet inaktivitet"), + ("Check for software update on startup", "Søg efter opdateringer ved opstart"), + ("upgrade_rustdesk_server_pro_to_{}_tip", "Opgradér venligst RustDesk Server Pro til version {} eller nyere!"), + ("pull_group_failed_tip", "Genindlæsning af gruppe mislykkedes"), + ("Filter by intersection", "Filtrér efter intersection"), + ("Remove wallpaper during incoming sessions", "Skjul baggrundsskærm ved indgående forbindelser"), + ("Test", "Test"), + ("display_is_plugged_out_msg", "Skærmen er slukket, skift til den første skærm."), + ("No displays", "Ingen skærme"), + ("Open in new window", "Åbn i et nyt vindue"), + ("Show displays as individual windows", "Vis skærme som selvstændige vinduer"), + ("Use all my displays for the remote session", "Brug alle mine skærme til fjernforbindelsen"), + ("selinux_tip", "SELinux er aktiveret på din enhed, som kan forhindre RustDesk i at køre normalt."), + ("Change view", "Skift visning"), + ("Big tiles", "Store fliser"), + ("Small tiles", "Små fliser"), + ("List", "Liste"), + ("Virtual display", "Virtuel skærm"), + ("Plug out all", "Frakobl alt"), + ("True color (4:4:4)", "True color (4:4:4)"), + ("Enable blocking user input", "Aktivér blokering af brugerstyring"), + ("id_input_tip", "Du kan indtaste ét ID, en direkte IP adresse, eller et domæne med en port (:).\nHvis du ønsker at forbinde til en enhed på en anden server, tilføj da server adressen (@?key=), fx,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nHvis du ønsker adgang til en enhed på en offentlig server, indtast venligst \"@offentlig server\", nøglen er ikke nødvendig for offentlige servere.\n\nHvis du gerne vil tvinge brugen af en relay-forbindelse på den første forbindelse, tilføj \"/r\" efter ID'et, fx, \"9123456234/r\"."), + ("privacy_mode_impl_mag_tip", "Tilstand 1"), + ("privacy_mode_impl_virtual_display_tip", "Tilstand 2"), + ("Enter privacy mode", "Start privatlivstilstand"), + ("Exit privacy mode", "Afslut privatlivstilstand"), + ("idd_not_support_under_win10_2004_tip", "Indirekte grafik drivere er ikke understøttet. Windows 10 version 2004 eller nyere er påkrævet."), + ("input_source_1_tip", "Input kilde 1"), + ("input_source_2_tip", "Input kilde 2"), + ("Swap control-command key", "Byt rundt på Control & Command tasterne"), + ("swap-left-right-mouse", "Byt rundt på venstre og højre musetaster"), + ("2FA code", "To-faktor kode"), + ("More", "Mere"), + ("enable-2fa-title", "Tænd for to-faktor godkendelse"), + ("enable-2fa-desc", "Åbn din godkendelsesapp nu. Du kan bruge en godkendelsesapp så som Authy, Microsoft eller Google Authenticator på din telefon eller din PC.\n\nScan QR koden med din app og indtast koden som din app fremviser, for at aktivere for to-faktor godkendelse."), + ("wrong-2fa-code", "Kan ikke verificere koden. Forsikr at koden og tidsindstillingerne på enheden er korrekte"), + ("enter-2fa-title", "To-faktor godkendelse"), + ("Email verification code must be 6 characters.", "E-mail bekræftelseskode skal være mindst 6 tegn"), + ("2FA code must be 6 digits.", "To-faktor kode skal være mindst 6 cifre"), + ("Multiple Windows sessions found", "Flere Windows sessioner fundet"), + ("Please select the session you want to connect to", "Vælg venligst sessionen du ønsker at forbinde til"), + ("powered_by_me", "Drives af RustDesk"), + ("outgoing_only_desk_tip", "Dette er en brugertilpasset udgave.\nDu kan forbinde til andre enheder, men andre enheder kan ikke forbinde til din enhed."), + ("preset_password_warning", "Denne brugertilpassede udgave har en forudbestemt adgangskode. Alle der kender til denne adgangskode, kan få fuld adgang til din enhed. Hvis du ikke forventede dette, bør du afinstallere denne udgave af RustDesk med det samme."), + ("Security Alert", "Sikkerhedsalarm"), + ("My address book", "Min adressebog"), + ("Personal", "Personlig"), + ("Owner", "Ejer"), + ("Set shared password", "Sæt delt adgangskode"), + ("Exist in", "Findes i"), + ("Read-only", "Skrivebeskyttet"), + ("Read/Write", "Læse/Skrive"), + ("Full Control", "Fuld kontrol"), + ("share_warning_tip", "Felterne for oven er delt og synlige for andre."), + ("Everyone", "Alle"), + ("ab_web_console_tip", "Mere på web konsollen"), + ("allow-only-conn-window-open-tip", "Tillad kun fjernforbindelser hvis RustDesk vinduet er synligt"), + ("no_need_privacy_mode_no_physical_displays_tip", "Ingen fysiske skærme, ingen nødvendighed for at bruge privatlivstilstanden."), + ("Follow remote cursor", "Følg musemarkør på fjernforbindelse"), + ("Follow remote window focus", "Følg vinduefokus på fjernforbindelse"), + ("default_proxy_tip", "Protokollen og porten som anvendes som standard er Socks5 og 1080"), + ("no_audio_input_device_tip", "Ingen lydinput enhed fundet"), + ("Incoming", "Indgående"), + ("Outgoing", "Udgående"), + ("Clear Wayland screen selection", "Ryd Wayland skærmvalg"), + ("clear_Wayland_screen_selection_tip", "Efter at fravælge den valgte skærm, kan du genvælge skærmen som skal deles."), + ("confirm_clear_Wayland_screen_selection_tip", "Er du sikker på at du vil fjerne Wayland skærmvalget?"), + ("android_new_voice_call_tip", "Du har modtaget en ny stemmeopkaldsforespørgsel. Hvis du accepterer, vil lyden skifte til stemmekommunikation."), + ("texture_render_tip", "Brug tekstur-rendering for at gøre billedkvaliteten blødere. Du kan også prøve at deaktivere denne funktion, hvis du oplever problemer."), + ("Use texture rendering", "Anvend tekstur-rendering"), + ("Floating window", "Svævende vindue"), + ("floating_window_tip", "Det hjælper på at RustDesk baggrundstjenesten kører"), + ("Keep screen on", "Hold skærmen tændt"), + ("Never", "Aldrig"), + ("During controlled", "Imens under kontrol"), + ("During service is on", "Imens tjenesten kører"), + ("Capture screen using DirectX", "Optag skærm med DirectX"), + ("Back", "Tilbage"), + ("Apps", "Apps"), + ("Volume up", "Skru op for lyd"), + ("Volume down", "Skru ned for lyd"), + ("Power", "Tænd/Sluk"), + ("Telegram bot", "Telegram bot"), + ("enable-bot-tip", "Hvis du aktiverer denne funktion, kan du modtage to-faktor godkendelseskoden fra din robot. Den kan også fungere som en notifikation for forbindelsesanmodninger."), + ("enable-bot-desc", "1. Åbn en chat med @BotFather.\n2. Send kommandoen \"/newbot\". Du vil modtage en nøgle efter at have gennemført dette trin.\n3. Start en chat med din nyoprettede bot. Send en besked som begynder med skråstreg \"/\", som fx \"/hello\", for at aktivere den.\n"), + ("cancel-2fa-confirm-tip", "Er du sikker på at du vil afbryde to-faktor godkendelse?"), + ("cancel-bot-confirm-tip", "Er du sikker på at du vil afbryde Telegram robotten?"), + ("About RustDesk", "Om RustDesk"), + ("Send clipboard keystrokes", "Send udklipsholder tastetryk"), + ("network_error_tip", "Tjek venligst din internetforbindelse, og forsøg igen."), + ("Unlock with PIN", "Lås op med PIN"), + ("Requires at least {} characters", "Kræver mindst {} tegn"), + ("Wrong PIN", "Forkert PIN"), + ("Set PIN", "Sæt PIN"), + ("Enable trusted devices", "Aktivér troværdige enheder"), + ("Manage trusted devices", "Administrér troværdige enheder"), + ("Platform", "Platform"), + ("Days remaining", "Dage tilbage"), + ("enable-trusted-devices-tip", "Spring to-faktor godkendelse over på troværdige enheder"), + ("Parent directory", "mappe"), + ("Resume", "Fortsæt"), + ("Invalid file name", "Ugyldigt filnavn"), ].iter().cloned().collect(); } From 39e713838f96e4ab20031831e74a16a22d37d3c2 Mon Sep 17 00:00:00 2001 From: 21pages Date: Tue, 3 Sep 2024 18:48:17 +0800 Subject: [PATCH 153/541] Use fallback codec if first frame fails (#9242) * Both encoding and decoding use fallback if first frame fails * More aggresive max fail counter * Update hwcodec, add judgement when length of the encoded data is zero, https://github.com/rustdesk/rustdesk-server-pro/discussions/382#discussioncomment-10525997 * Fix serde hwcodec config toml fails when the non-first vec![] is empty, https://github.com/toml-rs/toml-rs/issues/384, the config file is used for cache, when check process is not finished, the cache is used. * Allow cm not start for pro user Signed-off-by: 21pages --- Cargo.lock | 2 +- libs/scrap/src/common/hwcodec.rs | 19 +++++++++++++++++-- src/client.rs | 21 +++++++++++++++++++-- src/server/connection.rs | 2 ++ src/server/video_service.rs | 20 +++++++++++++++----- 5 files changed, 54 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fbf1f8ebb9c0..a6839b9eb127 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3045,7 +3045,7 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hwcodec" version = "0.7.0" -source = "git+https://github.com/rustdesk-org/hwcodec#6abd1898f3a03481ed0c038507b5218d6ea94267" +source = "git+https://github.com/rustdesk-org/hwcodec#b78a69c81631dd9ccfed9df68709808193082242" dependencies = [ "bindgen 0.59.2", "cc", diff --git a/libs/scrap/src/common/hwcodec.rs b/libs/scrap/src/common/hwcodec.rs index 4e653215eb6e..a0e730c91db6 100644 --- a/libs/scrap/src/common/hwcodec.rs +++ b/libs/scrap/src/common/hwcodec.rs @@ -498,6 +498,15 @@ pub struct HwCodecConfig { pub vram_decode: Vec, } +// HwCodecConfig2 is used to store the config in json format, +// confy can't serde HwCodecConfig successfully if the non-first struct Vec is empty due to old toml version. +// struct T { a: Vec, b: Vec} will fail if b is empty, but struct T { a: Vec, b: Vec} is ok. +#[derive(Debug, Default, Serialize, Deserialize, Clone)] +struct HwCodecConfig2 { + #[serde(default)] + pub config: String, +} + // ipc server process start check process once, other process get from ipc server once // install: --server start check process, check process send to --server, ui get from --server // portable: ui start check process, check process send to ui @@ -509,7 +518,12 @@ impl HwCodecConfig { log::info!("set hwcodec config"); log::debug!("{config:?}"); #[cfg(any(windows, target_os = "macos"))] - hbb_common::config::common_store(&config, "_hwcodec"); + hbb_common::config::common_store( + &HwCodecConfig2 { + config: serde_json::to_string_pretty(&config).unwrap_or_default(), + }, + "_hwcodec", + ); *CONFIG.lock().unwrap() = Some(config); *CONFIG_SET_BY_IPC.lock().unwrap() = true; } @@ -587,7 +601,8 @@ impl HwCodecConfig { Some(c) => c, None => { log::info!("try load cached hwcodec config"); - let c = hbb_common::config::common_load::("_hwcodec"); + let c = hbb_common::config::common_load::("_hwcodec"); + let c: HwCodecConfig = serde_json::from_str(&c.config).unwrap_or_default(); let new_signature = hwcodec::common::get_gpu_signature(); if c.signature == new_signature { log::debug!("load cached hwcodec config: {c:?}"); diff --git a/src/client.rs b/src/client.rs index b687c8a84c15..f1df8d20e48f 100644 --- a/src/client.rs +++ b/src/client.rs @@ -84,7 +84,7 @@ pub mod io_loop; pub const MILLI1: Duration = Duration::from_millis(1); pub const SEC30: Duration = Duration::from_secs(30); pub const VIDEO_QUEUE_SIZE: usize = 120; -const MAX_DECODE_FAIL_COUNTER: usize = 10; // Currently, failed decode cause refresh_video, so make it small +const MAX_DECODE_FAIL_COUNTER: usize = 3; #[cfg(target_os = "linux")] pub const LOGIN_MSG_DESKTOP_NOT_INITED: &str = "Desktop env is not inited"; @@ -1151,6 +1151,7 @@ pub struct VideoHandler { record: bool, _display: usize, // useful for debug fail_counter: usize, + first_frame: bool, } impl VideoHandler { @@ -1176,6 +1177,7 @@ impl VideoHandler { record: false, _display, fail_counter: 0, + first_frame: true, } } @@ -1204,9 +1206,19 @@ impl VideoHandler { self.fail_counter = 0; } else { if self.fail_counter < usize::MAX { - self.fail_counter += 1 + if self.first_frame && self.fail_counter < MAX_DECODE_FAIL_COUNTER { + log::error!("decode first frame failed"); + self.fail_counter = MAX_DECODE_FAIL_COUNTER; + } else { + self.fail_counter += 1; + } + log::error!( + "Failed to handle video frame, fail counter: {}", + self.fail_counter + ); } } + self.first_frame = false; if self.record { self.recorder .lock() @@ -1222,12 +1234,17 @@ impl VideoHandler { /// Reset the decoder, change format if it is Some pub fn reset(&mut self, format: Option) { + log::info!( + "reset video handler for display #{}, format: {format:?}", + self._display + ); #[cfg(target_os = "macos")] self.rgb.set_align(crate::get_dst_align_rgba()); let luid = Self::get_adapter_luid(); let format = format.unwrap_or(self.decoder.format()); self.decoder = Decoder::new(format, luid); self.fail_counter = 0; + self.first_frame = true; } /// Start or stop screen record. diff --git a/src/server/connection.rs b/src/server/connection.rs index 7b160ec2116f..3a2b0a22e914 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -1670,9 +1670,11 @@ impl Connection { .await { log::error!("ipc to connection manager exit: {}", err); + // https://github.com/rustdesk/rustdesk-server-pro/discussions/382#discussioncomment-10525725, cm may start failed #[cfg(windows)] if !crate::platform::is_prelogin() && !err.to_string().contains(crate::platform::EXPLORER_EXE) + && !crate::hbbs_http::sync::is_pro() { allow_err!(tx_from_cm_clone.send(Data::CmErr(err.to_string()))); } diff --git a/src/server/video_service.rs b/src/server/video_service.rs index 8b326a2ffdd3..e091966034be 100644 --- a/src/server/video_service.rs +++ b/src/server/video_service.rs @@ -486,6 +486,7 @@ fn run(vs: VideoService) -> ResultType<()> { let mut repeat_encode_counter = 0; let repeat_encode_max = 10; let mut encode_fail_counter = 0; + let mut first_frame = true; while sp.ok() { #[cfg(windows)] @@ -574,6 +575,7 @@ fn run(vs: VideoService) -> ResultType<()> { &mut encoder, recorder.clone(), &mut encode_fail_counter, + &mut first_frame, )?; frame_controller.set_send(now, send_conn_ids); } @@ -629,6 +631,7 @@ fn run(vs: VideoService) -> ResultType<()> { &mut encoder, recorder.clone(), &mut encode_fail_counter, + &mut first_frame, )?; frame_controller.set_send(now, send_conn_ids); } @@ -906,6 +909,7 @@ fn handle_one_frame( encoder: &mut Encoder, recorder: Arc>>, encode_fail_counter: &mut usize, + first_frame: &mut bool, ) -> ResultType> { sp.snapshot(|sps| { // so that new sub and old sub share the same encoder after switch @@ -917,6 +921,8 @@ fn handle_one_frame( })?; let mut send_conn_ids: HashSet = Default::default(); + let first = *first_frame; + *first_frame = false; match encoder.encode_to_message(frame, ms) { Ok(mut vf) => { *encode_fail_counter = 0; @@ -931,17 +937,21 @@ fn handle_one_frame( send_conn_ids = sp.send_video_frame(msg); } Err(e) => { + *encode_fail_counter += 1; + // Encoding errors are not frequent except on Android + if !cfg!(target_os = "android") { + log::error!("encode fail: {e:?}, times: {}", *encode_fail_counter,); + } let max_fail_times = if cfg!(target_os = "android") && encoder.is_hardware() { - 12 + 9 } else { - 6 + 3 }; - *encode_fail_counter += 1; - if *encode_fail_counter >= max_fail_times { + if first || *encode_fail_counter >= max_fail_times { *encode_fail_counter = 0; if encoder.is_hardware() { encoder.disable(); - log::error!("switch due to encoding fails more than {max_fail_times} times"); + log::error!("switch due to encoding fails, first frame: {first}, error: {e:?}"); bail!("SWITCH"); } } From d4377a13c5062d4d887f4768b39d1f54723db596 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Tue, 3 Sep 2024 19:06:11 +0800 Subject: [PATCH 154/541] refact: peer card, orientation (#9235) * refact: peer card, orientation Signed-off-by: fufesou * Do not change landscape/portrait on Desktop Signed-off-by: fufesou * comments Signed-off-by: fufesou --------- Signed-off-by: fufesou --- flutter/lib/common/widgets/address_book.dart | 53 ++++++------ flutter/lib/common/widgets/autocomplete.dart | 2 +- flutter/lib/common/widgets/dialog.dart | 3 +- flutter/lib/common/widgets/my_group.dart | 21 ++--- flutter/lib/common/widgets/peer_card.dart | 73 ++++++++--------- flutter/lib/common/widgets/peer_tab_page.dart | 73 ++++++++--------- flutter/lib/common/widgets/peers_view.dart | 80 +++++++++---------- flutter/lib/common/widgets/remote_input.dart | 4 +- flutter/lib/main.dart | 30 ++++++- flutter/lib/models/state_model.dart | 2 + 10 files changed, 186 insertions(+), 155 deletions(-) diff --git a/flutter/lib/common/widgets/address_book.dart b/flutter/lib/common/widgets/address_book.dart index 67b262cd11ac..a0a456807a86 100644 --- a/flutter/lib/common/widgets/address_book.dart +++ b/flutter/lib/common/widgets/address_book.dart @@ -11,6 +11,7 @@ import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/desktop/widgets/popup_menu.dart'; import 'package:flutter_hbb/models/ab_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 'package:get/get.dart'; @@ -61,15 +62,16 @@ class _AddressBookState extends State { retry: null, // remove retry close: () => gFFI.abModel.currentAbPushError.value = ''), Expanded( - child: (isDesktop || isWebDesktop) - ? _buildAddressBookDesktop() - : _buildAddressBookMobile()) + child: Obx(() => stateGlobal.isPortrait.isTrue + ? _buildAddressBookPortrait() + : _buildAddressBookLandscape()), + ), ], ); } }); - Widget _buildAddressBookDesktop() { + Widget _buildAddressBookLandscape() { return Row( children: [ Offstage( @@ -106,7 +108,7 @@ class _AddressBookState extends State { ); } - Widget _buildAddressBookMobile() { + Widget _buildAddressBookPortrait() { const padding = 8.0; return Column( children: [ @@ -239,14 +241,14 @@ class _AddressBookState extends State { bind.setLocalFlutterOption(k: kOptionCurrentAbName, v: value); } }, - customButton: Container( - height: isDesktop ? 48 : 40, + 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), @@ -335,8 +337,8 @@ class _AddressBookState extends State { showActionMenu: editPermission); } - final gridView = DynamicGridView.builder( - shrinkWrap: isMobile, + gridView(bool isPortrait) => DynamicGridView.builder( + shrinkWrap: isPortrait, gridDelegate: SliverGridDelegateWithWrapping(), itemCount: tags.length, itemBuilder: (BuildContext context, int index) { @@ -344,9 +346,9 @@ class _AddressBookState extends State { return tagBuilder(e); }); final maxHeight = max(MediaQuery.of(context).size.height / 6, 100.0); - return (isDesktop || isWebDesktop) - ? gridView - : LimitedBox(maxHeight: maxHeight, child: gridView); + return Obx(() => stateGlobal.isPortrait.isFalse + ? gridView(false) + : LimitedBox(maxHeight: maxHeight, child: gridView(true))); }); } @@ -506,9 +508,9 @@ class _AddressBookState extends State { double marginBottom = 4; row({required Widget lable, required Widget input}) { - return Row( + makeChild(bool isPortrait) => Row( children: [ - !isMobile + !isPortrait ? ConstrainedBox( constraints: const BoxConstraints(minWidth: 100), child: lable.marginOnly(right: 10)) @@ -519,7 +521,8 @@ class _AddressBookState extends State { child: input), ), ], - ).marginOnly(bottom: !isMobile ? 8 : 0); + ).marginOnly(bottom: !isPortrait ? 8 : 0); + return Obx(() => makeChild(stateGlobal.isPortrait.isTrue)); } return CustomAlertDialog( @@ -542,24 +545,24 @@ class _AddressBookState extends State { ), ], ), - input: TextField( + input: Obx(() => TextField( controller: idController, inputFormatters: [IDTextInputFormatter()], decoration: InputDecoration( - labelText: !isMobile ? null : translate('ID'), + labelText: stateGlobal.isPortrait.isFalse ? null : translate('ID'), errorText: errorMsg, errorMaxLines: 5), - )), + ))), row( lable: Text( translate('Alias'), style: style, ), - input: TextField( + input: Obx(() => TextField( controller: aliasController, decoration: InputDecoration( - labelText: !isMobile ? null : translate('Alias'), - )), + labelText: stateGlobal.isPortrait.isFalse ? null : translate('Alias'), + ),)), ), if (isCurrentAbShared) row( @@ -567,11 +570,11 @@ class _AddressBookState extends State { translate('Password'), style: style, ), - input: TextField( + input: Obx(() => TextField( controller: passwordController, obscureText: !passwordVisible, decoration: InputDecoration( - labelText: !isMobile ? null : translate('Password'), + labelText: stateGlobal.isPortrait.isFalse ? null : translate('Password'), suffixIcon: IconButton( icon: Icon( passwordVisible @@ -585,7 +588,7 @@ class _AddressBookState extends State { }, ), ), - )), + ),)), if (gFFI.abModel.currentAbTags.isNotEmpty) Align( alignment: Alignment.centerLeft, diff --git a/flutter/lib/common/widgets/autocomplete.dart b/flutter/lib/common/widgets/autocomplete.dart index 07a11904da17..978d053df4be 100644 --- a/flutter/lib/common/widgets/autocomplete.dart +++ b/flutter/lib/common/widgets/autocomplete.dart @@ -189,7 +189,7 @@ class AutocompletePeerTileState extends State { .map((e) => gFFI.abModel.getCurrentAbTagColor(e)) .toList(); return Tooltip( - message: isMobile + message: !(isDesktop || isWebDesktop) ? '' : widget.peer.tags.isNotEmpty ? '${translate('Tags')}: ${widget.peer.tags.join(', ')}' diff --git a/flutter/lib/common/widgets/dialog.dart b/flutter/lib/common/widgets/dialog.dart index 7cc76d6c6a1b..5a72f5dc2174 100644 --- a/flutter/lib/common/widgets/dialog.dart +++ b/flutter/lib/common/widgets/dialog.dart @@ -10,6 +10,7 @@ import 'package:flutter_hbb/common/widgets/setting_widgets.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:qr_flutter/qr_flutter.dart'; @@ -1123,7 +1124,7 @@ void showRequestElevationDialog( errorText: errPwd.isEmpty ? null : errPwd.value, ), ], - ).marginOnly(left: (isDesktop || isWebDesktop) ? 35 : 0), + ).marginOnly(left: stateGlobal.isPortrait.isFalse ? 35 : 0), ).marginOnly(top: 10), ], ), diff --git a/flutter/lib/common/widgets/my_group.dart b/flutter/lib/common/widgets/my_group.dart index 0d9cc007ccc0..2d26536eb8f1 100644 --- a/flutter/lib/common/widgets/my_group.dart +++ b/flutter/lib/common/widgets/my_group.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_hbb/common/hbbs/hbbs.dart'; import 'package:flutter_hbb/common/widgets/login.dart'; import 'package:flutter_hbb/common/widgets/peers_view.dart'; +import 'package:flutter_hbb/models/state_model.dart'; import 'package:get/get.dart'; import '../../common.dart'; @@ -45,15 +46,15 @@ class _MyGroupState extends State { retry: null, close: () => gFFI.groupModel.groupLoadError.value = ''), Expanded( - child: (isDesktop || isWebDesktop) - ? _buildDesktop() - : _buildMobile()) + child: Obx(() => stateGlobal.isPortrait.isTrue + ? _buildPortrait() + : _buildLandscape())), ], ); }); } - Widget _buildDesktop() { + Widget _buildLandscape() { return Row( children: [ Container( @@ -89,7 +90,7 @@ class _MyGroupState extends State { ); } - Widget _buildMobile() { + Widget _buildPortrait() { return Column( children: [ Container( @@ -159,14 +160,14 @@ class _MyGroupState extends State { } return true; }).toList(); - final listView = ListView.builder( - shrinkWrap: isMobile, + listView(bool isPortrait) => ListView.builder( + shrinkWrap: isPortrait, itemCount: items.length, itemBuilder: (context, index) => _buildUserItem(items[index])); var maxHeight = max(MediaQuery.of(context).size.height / 6, 100.0); - return (isDesktop || isWebDesktop) - ? listView - : LimitedBox(maxHeight: maxHeight, child: listView); + return Obx(() => stateGlobal.isPortrait.isFalse + ? listView(false) + : LimitedBox(maxHeight: maxHeight, child: listView(true))); }); } diff --git a/flutter/lib/common/widgets/peer_card.dart b/flutter/lib/common/widgets/peer_card.dart index f9dd73bbf6b8..7760f7a03ae7 100644 --- a/flutter/lib/common/widgets/peer_card.dart +++ b/flutter/lib/common/widgets/peer_card.dart @@ -4,6 +4,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_hbb/common/widgets/dialog.dart'; import 'package:flutter_hbb/consts.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:provider/provider.dart'; @@ -53,14 +54,11 @@ class _PeerCardState extends State<_PeerCard> @override Widget build(BuildContext context) { super.build(context); - if (isDesktop || isWebDesktop) { - return _buildDesktop(); - } else { - return _buildMobile(); - } + return Obx(() => + stateGlobal.isPortrait.isTrue ? _buildPortrait() : _buildLandscape()); } - Widget _buildMobile() { + Widget _buildPortrait() { final peer = super.widget.peer; final PeerTabModel peerTabModel = Provider.of(context); return Card( @@ -87,7 +85,7 @@ class _PeerCardState extends State<_PeerCard> )); } - Widget _buildDesktop() { + Widget _buildLandscape() { final PeerTabModel peerTabModel = Provider.of(context); final peer = super.widget.peer; var deco = Rx( @@ -140,13 +138,13 @@ class _PeerCardState extends State<_PeerCard> final greyStyle = TextStyle( fontSize: 11, color: Theme.of(context).textTheme.titleLarge?.color?.withOpacity(0.6)); - final child = Row( + makeChild(bool isPortrait) => Row( mainAxisSize: MainAxisSize.max, children: [ Container( decoration: BoxDecoration( color: str2color('${peer.id}${peer.platform}', 0x7f), - borderRadius: isMobile + borderRadius: isPortrait ? BorderRadius.circular(_tileRadius) : BorderRadius.only( topLeft: Radius.circular(_tileRadius), @@ -154,11 +152,11 @@ class _PeerCardState extends State<_PeerCard> ), ), alignment: Alignment.center, - width: isMobile ? 50 : 42, - height: isMobile ? 50 : null, + width: isPortrait ? 50 : 42, + height: isPortrait ? 50 : null, child: Stack( children: [ - getPlatformImage(peer.platform, size: isMobile ? 38 : 30) + getPlatformImage(peer.platform, size: isPortrait ? 38 : 30) .paddingAll(6), if (_shouldBuildPasswordIcon(peer)) Positioned( @@ -183,19 +181,19 @@ class _PeerCardState extends State<_PeerCard> child: Column( children: [ Row(children: [ - getOnline(isMobile ? 4 : 8, peer.online), + getOnline(isPortrait ? 4 : 8, peer.online), Expanded( child: Text( peer.alias.isEmpty ? formatID(peer.id) : peer.alias, overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.titleSmall, )), - ]).marginOnly(top: isMobile ? 0 : 2), + ]).marginOnly(top: isPortrait ? 0 : 2), Align( alignment: Alignment.centerLeft, child: Text( name, - style: isMobile ? null : greyStyle, + style: isPortrait ? null : greyStyle, textAlign: TextAlign.start, overflow: TextOverflow.ellipsis, ), @@ -203,9 +201,9 @@ class _PeerCardState extends State<_PeerCard> ], ).marginOnly(top: 2), ), - isMobile - ? checkBoxOrActionMoreMobile(peer) - : checkBoxOrActionMoreDesktop(peer, isTile: true), + isPortrait + ? checkBoxOrActionMorePortrait(peer) + : checkBoxOrActionMoreLandscape(peer, isTile: true), ], ).paddingOnly(left: 10.0, top: 3.0), ), @@ -216,28 +214,27 @@ class _PeerCardState extends State<_PeerCard> .map((e) => gFFI.abModel.getCurrentAbTagColor(e)) .toList(); return Tooltip( - message: isMobile + message: !(isDesktop || isWebDesktop) ? '' : peer.tags.isNotEmpty ? '${translate('Tags')}: ${peer.tags.join(', ')}' : '', child: Stack(children: [ - deco == null - ? child - : Obx( - () => Container( + Obx(() => deco == null + ? makeChild(stateGlobal.isPortrait.isTrue) + : Container( foregroundDecoration: deco.value, - child: child, + child: makeChild(stateGlobal.isPortrait.isTrue), ), ), if (colors.isNotEmpty) - Positioned( + Obx(()=> Positioned( top: 2, - right: isMobile ? 20 : 10, + right: stateGlobal.isPortrait.isTrue ? 20 : 10, child: CustomPaint( painter: TagPainter(radius: 3, colors: colors), ), - ) + )) ]), ); } @@ -316,7 +313,7 @@ class _PeerCardState extends State<_PeerCard> style: Theme.of(context).textTheme.titleSmall, )), ]).paddingSymmetric(vertical: 8)), - checkBoxOrActionMoreDesktop(peer, isTile: false), + checkBoxOrActionMoreLandscape(peer, isTile: false), ], ).paddingSymmetric(horizontal: 12.0), ) @@ -362,7 +359,7 @@ class _PeerCardState extends State<_PeerCard> } } - Widget checkBoxOrActionMoreMobile(Peer peer) { + Widget checkBoxOrActionMorePortrait(Peer peer) { final PeerTabModel peerTabModel = Provider.of(context); final selected = peerTabModel.isPeerSelected(peer.id); if (peerTabModel.multiSelectionMode) { @@ -390,7 +387,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 selected = peerTabModel.isPeerSelected(peer.id); if (peerTabModel.multiSelectionMode) { @@ -1257,9 +1254,9 @@ void _rdpDialog(String id) async { ), ], ).marginOnly(bottom: isDesktop ? 8 : 0), - Row( + Obx(() => Row( children: [ - (isDesktop || isWebDesktop) + stateGlobal.isPortrait.isFalse ? ConstrainedBox( constraints: const BoxConstraints(minWidth: 140), child: Text( @@ -1270,17 +1267,17 @@ void _rdpDialog(String id) async { Expanded( child: TextField( decoration: InputDecoration( - labelText: (isDesktop || isWebDesktop) + labelText: isDesktop ? null : translate('Username')), controller: userController, ), ), ], - ).marginOnly(bottom: (isDesktop || isWebDesktop) ? 8 : 0), - Row( + ).marginOnly(bottom: stateGlobal.isPortrait.isFalse ? 8 : 0)), + Obx(() => Row( children: [ - (isDesktop || isWebDesktop) + stateGlobal.isPortrait.isFalse ? ConstrainedBox( constraints: const BoxConstraints(minWidth: 140), child: Text( @@ -1292,7 +1289,7 @@ void _rdpDialog(String id) async { child: Obx(() => TextField( obscureText: secure.value, decoration: InputDecoration( - labelText: (isDesktop || isWebDesktop) + labelText: isDesktop ? null : translate('Password'), suffixIcon: IconButton( @@ -1304,7 +1301,7 @@ void _rdpDialog(String id) async { )), ), ], - ) + )) ], ), ), diff --git a/flutter/lib/common/widgets/peer_tab_page.dart b/flutter/lib/common/widgets/peer_tab_page.dart index 8fe73144999f..c941a1b93d49 100644 --- a/flutter/lib/common/widgets/peer_tab_page.dart +++ b/flutter/lib/common/widgets/peer_tab_page.dart @@ -16,6 +16,7 @@ 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/state_model.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; import 'package:provider/provider.dart'; @@ -114,26 +115,26 @@ class _PeerTabPageState extends State textBaseline: TextBaseline.ideographic, crossAxisAlignment: CrossAxisAlignment.start, children: [ - SizedBox( - height: 32, - child: Container( - padding: (isDesktop || isWebDesktop) - ? null - : EdgeInsets.symmetric(horizontal: 2), - child: selectionWrap(Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Expanded( - child: - visibleContextMenuListener(_createSwitchBar(context))), - if (isMobile) - ..._mobileRightActions(context) - else - ..._desktopRightActions(context) - ], - )), - ), - ).paddingOnly(right: (isDesktop || isWebDesktop) ? 12 : 0), + Obx(() => SizedBox( + height: 32, + child: Container( + padding: stateGlobal.isPortrait.isTrue + ? EdgeInsets.symmetric(horizontal: 2) + : null, + child: selectionWrap(Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: visibleContextMenuListener( + _createSwitchBar(context))), + if (stateGlobal.isPortrait.isTrue) + ..._portraitRightActions(context) + else + ..._landscapeRightActions(context) + ], + )), + ), + ).paddingOnly(right: stateGlobal.isPortrait.isTrue ? 0 : 12)), _createPeersView(), ], ); @@ -299,7 +300,7 @@ class _PeerTabPageState extends State } Widget visibleContextMenuListener(Widget child) { - if (isMobile) { + if (!(isDesktop || isWebDesktop)) { return GestureDetector( onLongPressDown: (e) { final x = e.globalPosition.dx; @@ -456,7 +457,7 @@ class _PeerTabPageState extends State showToast(translate('Successful')); }, child: Icon(PeerTabModel.icons[PeerTabIndex.fav.index]), - ).marginOnly(left: isMobile ? 11 : 6), + ).marginOnly(left: !(isDesktop || isWebDesktop) ? 11 : 6), ); } @@ -477,7 +478,7 @@ class _PeerTabPageState extends State model.setMultiSelectionMode(false); }, child: Icon(PeerTabModel.icons[PeerTabIndex.ab.index]), - ).marginOnly(left: isMobile ? 11 : 6), + ).marginOnly(left: !(isDesktop || isWebDesktop) ? 11 : 6), ); } @@ -500,7 +501,7 @@ class _PeerTabPageState extends State }); }, child: Icon(Icons.tag)) - .marginOnly(left: isMobile ? 11 : 6), + .marginOnly(left: !(isDesktop || isWebDesktop) ? 11 : 6), ); } @@ -556,10 +557,10 @@ class _PeerTabPageState extends State }); } - List _desktopRightActions(BuildContext context) { + List _landscapeRightActions(BuildContext context) { final model = Provider.of(context); return [ - const PeerSearchBar().marginOnly(right: isMobile ? 0 : 13), + const PeerSearchBar().marginOnly(right: 13), _createRefresh( index: PeerTabIndex.ab, loading: gFFI.abModel.currentAbLoading), _createRefresh( @@ -580,7 +581,7 @@ class _PeerTabPageState extends State ]; } - List _mobileRightActions(BuildContext context) { + List _portraitRightActions(BuildContext context) { final model = Provider.of(context); final screenWidth = MediaQuery.of(context).size.width; final leftIconSize = Theme.of(context).iconTheme.size ?? 24; @@ -701,13 +702,13 @@ class _PeerSearchBarState extends State { baseOffset: 0, extentOffset: peerSearchTextController.value.text.length); }); - return Container( - width: isMobile ? 120 : 140, - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.background, - borderRadius: BorderRadius.circular(6), - ), - child: Obx(() => Row( + return Obx(() => Container( + width: stateGlobal.isPortrait.isTrue ? 120 : 140, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.background, + borderRadius: BorderRadius.circular(6), + ), + child: Row( children: [ Expanded( child: Row( @@ -768,8 +769,8 @@ class _PeerSearchBarState extends State { ), ) ], - )), - ); + ), + )); } } diff --git a/flutter/lib/common/widgets/peers_view.dart b/flutter/lib/common/widgets/peers_view.dart index ef9647eaa93d..befc73338ee0 100644 --- a/flutter/lib/common/widgets/peers_view.dart +++ b/flutter/lib/common/widgets/peers_view.dart @@ -6,6 +6,7 @@ import 'package:flutter/foundation.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/models/state_model.dart'; import 'package:get/get.dart'; import 'package:provider/provider.dart'; import 'package:visibility_detector/visibility_detector.dart'; @@ -128,7 +129,7 @@ class _PeersViewState extends State<_PeersView> @override void didChangeAppLifecycleState(AppLifecycleState state) { super.didChangeAppLifecycleState(state); - if (isDesktop) return; + if (isDesktop || isWebDesktop) return; if (state == AppLifecycleState.resumed) { _isActive = true; _queryCount = 0; @@ -194,7 +195,7 @@ class _PeersViewState extends State<_PeersView> var peers = snapshot.data!; if (peers.length > 1000) peers = peers.sublist(0, 1000); gFFI.peerTabModel.setCurrentTabCachedPeers(peers); - buildOnePeer(Peer peer) { + buildOnePeer(Peer peer, bool isPortrait) { final visibilityChild = VisibilityDetector( key: ValueKey(_cardId(peer.id)), onVisibilityChanged: onVisibilityChanged, @@ -206,7 +207,7 @@ class _PeersViewState extends State<_PeersView> // No need to listen the currentTab change event. // Because the currentTab change event will trigger the peers change event, // and the peers change event will trigger _buildPeersView(). - return (isDesktop || isWebDesktop) + return !isPortrait ? Obx(() => peerCardUiType.value == PeerUiType.list ? Container(height: 45, child: visibilityChild) : peerCardUiType.value == PeerUiType.grid @@ -217,44 +218,41 @@ class _PeersViewState extends State<_PeersView> : Container(child: visibilityChild); } - final Widget child; - if (isMobile) { - child = ListView.builder( - itemCount: peers.length, - itemBuilder: (BuildContext context, int index) { - return buildOnePeer(peers[index]).marginOnly( - top: index == 0 ? 0 : space / 2, bottom: space / 2); - }, - ); - } else { - child = Obx(() => 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]).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]); - }), - )); - } + final Widget child = Obx(() => stateGlobal.isPortrait.isTrue + ? ListView.builder( + itemCount: peers.length, + itemBuilder: (BuildContext context, int 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) { _curPeers.clear(); diff --git a/flutter/lib/common/widgets/remote_input.dart b/flutter/lib/common/widgets/remote_input.dart index fb19c4c23456..a4d3caf2990a 100644 --- a/flutter/lib/common/widgets/remote_input.dart +++ b/flutter/lib/common/widgets/remote_input.dart @@ -243,7 +243,7 @@ class _RawTouchGestureDetectorRegionState if (ffi.cursorModel.shouldBlock(d.localPosition.dx, d.localPosition.dy)) { return; } - if (isDesktop) { + if (isDesktop || isWebDesktop) { ffi.cursorModel.trySetRemoteWindowCoords(); } // Workaround for the issue that the first pan event is sent a long time after the start event. @@ -285,7 +285,7 @@ class _RawTouchGestureDetectorRegionState if (lastDeviceKind != PointerDeviceKind.touch) { return; } - if (isDesktop) { + if (isDesktop || isWebDesktop) { ffi.cursorModel.clearRemoteWindowCoords(); } inputModel.sendMouse('up', MouseButtons.left); diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index c2009bcae13a..dc02ac81fdac 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -372,7 +372,7 @@ class App extends StatefulWidget { State createState() => _AppState(); } -class _AppState extends State { +class _AppState extends State with WidgetsBindingObserver { @override void initState() { super.initState(); @@ -396,6 +396,34 @@ class _AppState extends State { bind.mainChangeTheme(dark: to.toShortString()); } }; + WidgetsBinding.instance.addObserver(this); + WidgetsBinding.instance.addPostFrameCallback((_) => _updateOrientation()); + } + + @override + void dispose() { + WidgetsBinding.instance.removeObserver(this); + super.dispose(); + } + + @override + void didChangeMetrics() { + _updateOrientation(); + } + + void _updateOrientation() { + if (isDesktop) return; + + // Don't use `MediaQuery.of(context).orientation` in `didChangeMetrics()`, + // my test (Flutter 3.19.6, Android 14) is always the reverse value. + // https://github.com/flutter/flutter/issues/60899 + // stateGlobal.isPortrait.value = + // MediaQuery.of(context).orientation == Orientation.portrait; + + final orientation = View.of(context).physicalSize.aspectRatio > 1 + ? Orientation.landscape + : Orientation.portrait; + stateGlobal.isPortrait.value = orientation == Orientation.portrait; } @override diff --git a/flutter/lib/models/state_model.dart b/flutter/lib/models/state_model.dart index 7c4d3cfd0596..e18874785cf6 100644 --- a/flutter/lib/models/state_model.dart +++ b/flutter/lib/models/state_model.dart @@ -20,6 +20,8 @@ class StateGlobal { final svcStatus = SvcStatus.notReady.obs; final RxBool isFocused = false.obs; + final isPortrait = false.obs; + String _inputSource = ''; // Use for desktop -> remote toolbar -> resolution From ec28567362e1a0f30cb20df42f66f6f6b221c95b Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Tue, 3 Sep 2024 20:55:45 +0800 Subject: [PATCH 155/541] fix: win, file clipboard (#9243) 1. Return the result of `wait_response_event()` in `cliprdr_send_format_list()` 2. Add recv flags to avoid waiting a long time. Signed-off-by: fufesou --- libs/clipboard/src/lib.rs | 19 +++++--- libs/clipboard/src/platform/windows.rs | 63 +++++++++++++++++-------- libs/clipboard/src/windows/wf_cliprdr.c | 41 +++++++++++----- src/client/io_loop.rs | 1 + 4 files changed, 86 insertions(+), 38 deletions(-) diff --git a/libs/clipboard/src/lib.rs b/libs/clipboard/src/lib.rs index 1a9a047578f8..a5da25512d2a 100644 --- a/libs/clipboard/src/lib.rs +++ b/libs/clipboard/src/lib.rs @@ -5,7 +5,7 @@ use std::{ }; #[cfg(any(target_os = "windows", feature = "unix-file-copy-paste",))] -use hbb_common::{allow_err, log}; +use hbb_common::{allow_err, bail}; use hbb_common::{ lazy_static, tokio::sync::{ @@ -25,6 +25,8 @@ pub use context_send::*; const ERR_CODE_SERVER_FUNCTION_NONE: u32 = 0x00000001; #[cfg(target_os = "windows")] const ERR_CODE_INVALID_PARAMETER: u32 = 0x00000002; +#[cfg(target_os = "windows")] +const ERR_CODE_SEND_MSG: u32 = 0x00000003; pub(crate) use platform::create_cliprdr_context; @@ -198,7 +200,7 @@ pub fn get_rx_cliprdr_server(conn_id: i32) -> Arc ResultType<()> { #[cfg(target_os = "windows")] return send_data_to_channel(conn_id, data); #[cfg(not(target_os = "windows"))] @@ -210,25 +212,28 @@ fn send_data(conn_id: i32, data: ClipboardFile) { } #[cfg(any(target_os = "windows", feature = "unix-file-copy-paste",))] #[inline] -fn send_data_to_channel(conn_id: i32, data: ClipboardFile) { - // no need to handle result here +fn send_data_to_channel(conn_id: i32, data: ClipboardFile) -> ResultType<()> { if let Some(msg_channel) = VEC_MSG_CHANNEL .read() .unwrap() .iter() .find(|x| x.conn_id == conn_id) { - allow_err!(msg_channel.sender.send(data)); + msg_channel.sender.send(data)?; + Ok(()) + } else { + bail!("conn_id not found"); } } #[cfg(feature = "unix-file-copy-paste")] #[inline] -fn send_data_to_all(data: ClipboardFile) { - // no need to handle result here +fn send_data_to_all(data: ClipboardFile) -> ResultType<()> { + // Need more tests to see if it's necessary to handle the error. for msg_channel in VEC_MSG_CHANNEL.read().unwrap().iter() { allow_err!(msg_channel.sender.send(data.clone())); } + Ok(()) } #[cfg(test)] diff --git a/libs/clipboard/src/platform/windows.rs b/libs/clipboard/src/platform/windows.rs index 8fc917c6fee4..5d1aa086ddbf 100644 --- a/libs/clipboard/src/platform/windows.rs +++ b/libs/clipboard/src/platform/windows.rs @@ -7,7 +7,7 @@ use crate::{ allow_err, send_data, ClipboardFile, CliprdrError, CliprdrServiceContext, ResultType, - ERR_CODE_INVALID_PARAMETER, ERR_CODE_SERVER_FUNCTION_NONE, VEC_MSG_CHANNEL, + ERR_CODE_INVALID_PARAMETER, ERR_CODE_SEND_MSG, ERR_CODE_SERVER_FUNCTION_NONE, VEC_MSG_CHANNEL, }; use hbb_common::log; use std::{ @@ -998,7 +998,7 @@ extern "C" fn notify_callback(conn_id: UINT32, msg: *const NOTIFICATION_MESSAGE) } }; // no need to handle result here - send_data(conn_id as _, data); + allow_err!(send_data(conn_id as _, data)); 0 } @@ -1045,7 +1045,13 @@ extern "C" fn client_format_list( .iter() .for_each(|msg_channel| allow_err!(msg_channel.sender.send(data.clone()))); } else { - send_data(conn_id, data); + match send_data(conn_id, data) { + Ok(_) => {} + Err(e) => { + log::error!("failed to send format list: {:?}", e); + return ERR_CODE_SEND_MSG; + } + } } 0 @@ -1067,9 +1073,13 @@ extern "C" fn client_format_list_response( msg_flags ); let data = ClipboardFile::FormatListResponse { msg_flags }; - send_data(conn_id, data); - - 0 + match send_data(conn_id, data) { + Ok(_) => 0, + Err(e) => { + log::error!("failed to send format list response: {:?}", e); + ERR_CODE_SEND_MSG + } + } } extern "C" fn client_format_data_request( @@ -1090,10 +1100,13 @@ extern "C" fn client_format_data_request( conn_id, requested_format_id ); - // no need to handle result here - send_data(conn_id, data); - - 0 + match send_data(conn_id, data) { + Ok(_) => 0, + Err(e) => { + log::error!("failed to send format data request: {:?}", e); + ERR_CODE_SEND_MSG + } + } } extern "C" fn client_format_data_response( @@ -1125,9 +1138,13 @@ extern "C" fn client_format_data_response( msg_flags, format_data, }; - send_data(conn_id, data); - - 0 + match send_data(conn_id, data) { + Ok(_) => 0, + Err(e) => { + log::error!("failed to send format data response: {:?}", e); + ERR_CODE_SEND_MSG + } + } } extern "C" fn client_file_contents_request( @@ -1175,9 +1192,13 @@ extern "C" fn client_file_contents_request( clip_data_id, }; log::debug!("client_file_contents_request called, data: {:?}", &data); - send_data(conn_id, data); - - 0 + match send_data(conn_id, data) { + Ok(_) => 0, + Err(e) => { + log::error!("failed to send file contents request: {:?}", e); + ERR_CODE_SEND_MSG + } + } } extern "C" fn client_file_contents_response( @@ -1213,7 +1234,11 @@ extern "C" fn client_file_contents_response( msg_flags, stream_id ); - send_data(conn_id, data); - - 0 + match send_data(conn_id, data) { + Ok(_) => 0, + Err(e) => { + log::error!("failed to send file contents response: {:?}", e); + ERR_CODE_SEND_MSG + } + } } diff --git a/libs/clipboard/src/windows/wf_cliprdr.c b/libs/clipboard/src/windows/wf_cliprdr.c index c8f2038a1d28..5bca5405274e 100644 --- a/libs/clipboard/src/windows/wf_cliprdr.c +++ b/libs/clipboard/src/windows/wf_cliprdr.c @@ -220,7 +220,8 @@ struct wf_clipboard HWND hwnd; HANDLE hmem; HANDLE thread; - HANDLE response_data_event; + HANDLE formatDataRespEvent; + BOOL formatDataRespReceived; LPDATAOBJECT data_obj; HANDLE data_obj_mutex; @@ -228,6 +229,7 @@ struct wf_clipboard ULONG req_fsize; char *req_fdata; HANDLE req_fevent; + BOOL req_f_received; size_t nFiles; size_t file_array_size; @@ -1444,7 +1446,7 @@ static UINT cliprdr_send_format_list(wfClipboard *clipboard, UINT32 connID) return rc; } -UINT wait_response_event(UINT32 connID, wfClipboard *clipboard, HANDLE event, void **data) +UINT wait_response_event(UINT32 connID, wfClipboard *clipboard, HANDLE event, BOOL* recvedFlag, void **data) { UINT rc = ERROR_SUCCESS; clipboard->context->IsStopped = FALSE; @@ -1456,7 +1458,14 @@ UINT wait_response_event(UINT32 connID, wfClipboard *clipboard, HANDLE event, vo DWORD waitRes = WaitForSingleObject(event, waitOnceTimeoutMillis); if (waitRes == WAIT_TIMEOUT && clipboard->context->IsStopped == FALSE) { - continue; + if ((*recvedFlag) == TRUE) { + // The data has been received, but the event is still not signaled. + // We just skip the rest of the waiting and reset the flag. + *recvedFlag = FALSE; + } else { + // The data has not been received yet, we should continue to wait. + continue; + } } if (clipboard->context->IsStopped == TRUE) @@ -1524,13 +1533,14 @@ static UINT cliprdr_send_data_request(UINT32 connID, wfClipboard *clipboard, UIN formatDataRequest.connID = connID; formatDataRequest.requestedFormatId = remoteFormatId; clipboard->requestedFormatId = formatId; + clipboard->formatDataRespReceived = FALSE; rc = clipboard->context->ClientFormatDataRequest(clipboard->context, &formatDataRequest); if (rc != ERROR_SUCCESS) { return rc; } - wait_response_event(connID, clipboard, clipboard->response_data_event, &clipboard->hmem); + return wait_response_event(connID, clipboard, clipboard->formatDataRespEvent, &clipboard->formatDataRespReceived, &clipboard->hmem); } UINT cliprdr_send_request_filecontents(wfClipboard *clipboard, UINT32 connID, const void *streamid, ULONG index, @@ -1552,13 +1562,14 @@ UINT cliprdr_send_request_filecontents(wfClipboard *clipboard, UINT32 connID, co fileContentsRequest.cbRequested = nreq; fileContentsRequest.clipDataId = 0; fileContentsRequest.msgFlags = 0; + clipboard->req_f_received = FALSE; rc = clipboard->context->ClientFileContentsRequest(clipboard->context, &fileContentsRequest); if (rc != ERROR_SUCCESS) { return rc; } - return wait_response_event(connID, clipboard, clipboard->req_fevent, (void **)&clipboard->req_fdata); + return wait_response_event(connID, clipboard, clipboard->req_fevent, &clipboard->req_f_received, (void **)&clipboard->req_fdata); } static UINT cliprdr_send_response_filecontents( @@ -2545,7 +2556,7 @@ wf_cliprdr_server_format_data_request(CliprdrClientContext *context, response.requestedFormatData = (BYTE *)buff; if (ERROR_SUCCESS != clipboard->context->ClientFormatDataResponse(clipboard->context, &response)) { - // CAUTION: if failed to send, server will wait a long time + // CAUTION: if failed to send, server will wait a long time, default 30 seconds. } if (buff) @@ -2621,9 +2632,11 @@ wf_cliprdr_server_format_data_response(CliprdrClientContext *context, rc = CHANNEL_RC_OK; } while (0); - if (!SetEvent(clipboard->response_data_event)) + if (!SetEvent(clipboard->formatDataRespEvent)) { - // CAUTION: critical error here, process will hang up until wait timeout default 3min. + // If failed to set event, set flag to indicate the event is received. + DEBUG_CLIPRDR("wf_cliprdr_server_format_data_response(), SetEvent failed with 0x%x", GetLastError()); + clipboard->formatDataRespReceived = TRUE; rc = ERROR_INTERNAL_ERROR; } return rc; @@ -2899,7 +2912,9 @@ wf_cliprdr_server_file_contents_response(CliprdrClientContext *context, if (!SetEvent(clipboard->req_fevent)) { - // CAUTION: critical error here, process will hang up until wait timeout default 3min. + // If failed to set event, set flag to indicate the event is received. + DEBUG_CLIPRDR("wf_cliprdr_server_file_contents_response(), SetEvent failed with 0x%x", GetLastError()); + clipboard->req_f_received = TRUE; } return rc; } @@ -2934,14 +2949,16 @@ BOOL wf_cliprdr_init(wfClipboard *clipboard, CliprdrClientContext *cliprdr) (formatMapping *)calloc(clipboard->map_capacity, sizeof(formatMapping)))) goto error; - if (!(clipboard->response_data_event = CreateEvent(NULL, TRUE, FALSE, NULL))) + if (!(clipboard->formatDataRespEvent = CreateEvent(NULL, TRUE, FALSE, NULL))) goto error; + clipboard->formatDataRespReceived = FALSE; if (!(clipboard->data_obj_mutex = CreateMutex(NULL, FALSE, "data_obj_mutex"))) goto error; if (!(clipboard->req_fevent = CreateEvent(NULL, TRUE, FALSE, NULL))) goto error; + clipboard->req_f_received = FALSE; if (!(clipboard->thread = CreateThread(NULL, 0, cliprdr_thread_func, clipboard, 0, NULL))) goto error; @@ -3002,8 +3019,8 @@ BOOL wf_cliprdr_uninit(wfClipboard *clipboard, CliprdrClientContext *cliprdr) clipboard->data_obj = NULL; } - if (clipboard->response_data_event) - CloseHandle(clipboard->response_data_event); + if (clipboard->formatDataRespEvent) + CloseHandle(clipboard->formatDataRespEvent); if (clipboard->data_obj_mutex) CloseHandle(clipboard->data_obj_mutex); diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index b222e411815c..1a209ca0a533 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -353,6 +353,7 @@ impl Remote { } else { if let Err(e) = ContextSend::make_sure_enabled() { log::error!("failed to restart clipboard context: {}", e); + // to-do: Show msgbox with "Don't show again" option }; log::debug!("Send system clipboard message to remote"); let msg = crate::clipboard_file::clip_2_msg(clip); From 04c0f66ca983e80acc0c330246208e32e3fc35ea Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Tue, 3 Sep 2024 21:15:35 +0800 Subject: [PATCH 156/541] fix: set to OK if recv flag is TRUE (#9244) Signed-off-by: fufesou --- libs/clipboard/src/windows/wf_cliprdr.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/libs/clipboard/src/windows/wf_cliprdr.c b/libs/clipboard/src/windows/wf_cliprdr.c index 5bca5405274e..1f65b1913b2d 100644 --- a/libs/clipboard/src/windows/wf_cliprdr.c +++ b/libs/clipboard/src/windows/wf_cliprdr.c @@ -1462,6 +1462,8 @@ UINT wait_response_event(UINT32 connID, wfClipboard *clipboard, HANDLE event, BO // The data has been received, but the event is still not signaled. // We just skip the rest of the waiting and reset the flag. *recvedFlag = FALSE; + // Explicitly set the waitRes to WAIT_OBJECT_0, because we have received the data. + waitRes = WAIT_OBJECT_0; } else { // The data has not been received yet, we should continue to wait. continue; From 29e12b84a9f24d57bb6a197774a299686b0e1625 Mon Sep 17 00:00:00 2001 From: 21pages Date: Wed, 4 Sep 2024 11:31:13 +0800 Subject: [PATCH 157/541] password max length prompt (#9248) Signed-off-by: 21pages --- flutter/lib/common/widgets/dialog.dart | 9 +++++++ flutter/lib/common/widgets/peer_card.dart | 2 ++ .../lib/desktop/pages/desktop_home_page.dart | 3 +++ .../desktop/pages/desktop_setting_page.dart | 1 + libs/hbb_common/src/config.rs | 2 +- libs/hbb_common/src/password_security.rs | 10 +++---- src/flutter_ffi.rs | 4 +++ src/ipc.rs | 27 ++++++++++++------- src/ui_interface.rs | 5 ++++ 9 files changed, 47 insertions(+), 16 deletions(-) diff --git a/flutter/lib/common/widgets/dialog.dart b/flutter/lib/common/widgets/dialog.dart index 5a72f5dc2174..cc3e0613105b 100644 --- a/flutter/lib/common/widgets/dialog.dart +++ b/flutter/lib/common/widgets/dialog.dart @@ -381,6 +381,7 @@ class DialogTextField extends StatelessWidget { final FocusNode? focusNode; final TextInputType? keyboardType; final List? inputFormatters; + final int? maxLength; static const kUsernameTitle = 'Username'; static const kUsernameIcon = Icon(Icons.account_circle_outlined); @@ -398,6 +399,7 @@ class DialogTextField extends StatelessWidget { this.hintText, this.keyboardType, this.inputFormatters, + this.maxLength, required this.title, required this.controller}) : super(key: key); @@ -424,6 +426,7 @@ class DialogTextField extends StatelessWidget { obscureText: obscureText, keyboardType: keyboardType, inputFormatters: inputFormatters, + maxLength: maxLength, ), ), ], @@ -681,6 +684,7 @@ class PasswordWidget extends StatefulWidget { this.hintText, this.errorText, this.title, + this.maxLength, }) : super(key: key); final TextEditingController controller; @@ -689,6 +693,7 @@ class PasswordWidget extends StatefulWidget { final String? hintText; final String? errorText; final String? title; + final int? maxLength; @override State createState() => _PasswordWidgetState(); @@ -751,6 +756,7 @@ class _PasswordWidgetState extends State { obscureText: !_passwordVisible, errorText: widget.errorText, focusNode: _focusNode, + maxLength: widget.maxLength, ); } } @@ -2245,6 +2251,7 @@ void changeUnlockPinDialog(String oldPin, Function() callback) { final confirmController = TextEditingController(text: oldPin); String? pinErrorText; String? confirmationErrorText; + final maxLength = bind.mainMaxEncryptLen(); gFFI.dialogManager.show((setState, close, context) { submit() async { pinErrorText = null; @@ -2278,12 +2285,14 @@ void changeUnlockPinDialog(String oldPin, Function() callback) { controller: pinController, obscureText: true, errorText: pinErrorText, + maxLength: maxLength, ), DialogTextField( title: translate('Confirmation'), controller: confirmController, obscureText: true, errorText: confirmationErrorText, + maxLength: maxLength, ) ], ).marginOnly(bottom: 12), diff --git a/flutter/lib/common/widgets/peer_card.dart b/flutter/lib/common/widgets/peer_card.dart index 7760f7a03ae7..15ca8932d1f3 100644 --- a/flutter/lib/common/widgets/peer_card.dart +++ b/flutter/lib/common/widgets/peer_card.dart @@ -1200,6 +1200,7 @@ class MyGroupPeerCard extends BasePeerCard { } void _rdpDialog(String id) async { + final maxLength = bind.mainMaxEncryptLen(); final port = await bind.mainGetPeerOption(id: id, key: 'rdp_port'); final username = await bind.mainGetPeerOption(id: id, key: 'rdp_username'); final portController = TextEditingController(text: port); @@ -1288,6 +1289,7 @@ void _rdpDialog(String id) async { Expanded( child: Obx(() => TextField( obscureText: secure.value, + maxLength: maxLength, decoration: InputDecoration( labelText: isDesktop ? null diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index 31a8e1374ff5..dee990af4c2d 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -857,6 +857,7 @@ void setPasswordDialog({VoidCallback? notEmptyCallback}) async { // SpecialCharacterValidationRule(), MinCharactersValidationRule(8), ]; + final maxLength = bind.mainMaxEncryptLen(); gFFI.dialogManager.show((setState, close, context) { submit() { @@ -915,6 +916,7 @@ void setPasswordDialog({VoidCallback? notEmptyCallback}) async { errMsg0 = ''; }); }, + maxLength: maxLength, ), ), ], @@ -941,6 +943,7 @@ void setPasswordDialog({VoidCallback? notEmptyCallback}) async { errMsg1 = ''; }); }, + maxLength: maxLength, ), ), ], diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 0e5fac4caf58..041b5569ed51 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -2512,6 +2512,7 @@ void changeSocks5Proxy() async { : Icons.visibility))), controller: pwdController, enabled: !isOptFixed, + maxLength: bind.mainMaxEncryptLen(), )), ), ], diff --git a/libs/hbb_common/src/config.rs b/libs/hbb_common/src/config.rs index d0b908b551c2..c118070dd851 100644 --- a/libs/hbb_common/src/config.rs +++ b/libs/hbb_common/src/config.rs @@ -39,7 +39,7 @@ pub const REG_INTERVAL: i64 = 15_000; pub const COMPRESS_LEVEL: i32 = 3; const SERIAL: i32 = 3; const PASSWORD_ENC_VERSION: &str = "00"; -const ENCRYPT_MAX_LEN: usize = 128; +pub const ENCRYPT_MAX_LEN: usize = 128; // used for password, pin, etc, not for all #[cfg(target_os = "macos")] lazy_static::lazy_static! { diff --git a/libs/hbb_common/src/password_security.rs b/libs/hbb_common/src/password_security.rs index 49a2d4d9498e..5c04cc97b928 100644 --- a/libs/hbb_common/src/password_security.rs +++ b/libs/hbb_common/src/password_security.rs @@ -89,11 +89,11 @@ pub fn encrypt_str_or_original(s: &str, version: &str, max_len: usize) -> String log::error!("Duplicate encryption!"); return s.to_owned(); } - if s.bytes().len() > max_len { + if s.chars().count() > max_len { return String::default(); } if version == "00" { - if let Ok(s) = encrypt(s.as_bytes(), max_len) { + if let Ok(s) = encrypt(s.as_bytes()) { return version.to_owned() + &s; } } @@ -130,7 +130,7 @@ pub fn encrypt_vec_or_original(v: &[u8], version: &str, max_len: usize) -> Vec (Vec, boo (v.to_owned(), false, !v.is_empty()) } -fn encrypt(v: &[u8], max_len: usize) -> Result { - if !v.is_empty() && v.len() <= max_len { +fn encrypt(v: &[u8]) -> Result { + if !v.is_empty() { symmetric_crypt(v, true).map(|v| base64::encode(v, base64::Variant::Original)) } else { Err(()) diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 8cc475316142..72b0e5b37b42 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -2282,6 +2282,10 @@ pub fn main_clear_trusted_devices() { clear_trusted_devices() } +pub fn main_max_encrypt_len() -> SyncReturn { + SyncReturn(max_encrypt_len()) +} + pub fn session_request_new_display_init_msgs(session_id: SessionID, display: usize) { if let Some(session) = sessions::get_session_by_session_id(&session_id) { session.request_init_msgs(display); diff --git a/src/ipc.rs b/src/ipc.rs index 3f093c758e2d..6d5e247d42ce 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -928,16 +928,23 @@ pub fn set_permanent_password(v: String) -> ResultType<()> { pub fn set_unlock_pin(v: String, translate: bool) -> ResultType<()> { let v = v.trim().to_owned(); let min_len = 4; - if !v.is_empty() && v.len() < min_len { - let err = if translate { - crate::lang::translate( - "Requires at least {".to_string() + &format!("{min_len}") + "} characters", - ) - } else { - // Sometimes, translated can't show normally in command line - format!("Requires at least {} characters", min_len) - }; - bail!(err); + let max_len = crate::ui_interface::max_encrypt_len(); + let len = v.chars().count(); + if !v.is_empty() { + if len < min_len { + let err = if translate { + crate::lang::translate( + "Requires at least {".to_string() + &format!("{min_len}") + "} characters", + ) + } else { + // Sometimes, translated can't show normally in command line + format!("Requires at least {} characters", min_len) + }; + bail!(err); + } + if len > max_len { + bail!("No more than {max_len} characters"); + } } Config::set_unlock_pin(&v); set_config("unlock-pin", v) diff --git a/src/ui_interface.rs b/src/ui_interface.rs index 8489179ba67b..aad968bb4a51 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -1495,3 +1495,8 @@ pub fn clear_trusted_devices() { #[cfg(not(any(target_os = "android", target_os = "ios")))] ipc::clear_trusted_devices(); } + +#[cfg(feature = "flutter")] +pub fn max_encrypt_len() -> usize { + hbb_common::config::ENCRYPT_MAX_LEN +} From dbbbd08934bc52cdc455ca7edbfe512359218454 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Wed, 4 Sep 2024 16:44:36 +0800 Subject: [PATCH 158/541] fix: clipboard, support excel xml spreadsheet (#9252) Signed-off-by: fufesou --- libs/hbb_common/protos/message.proto | 3 +++ src/clipboard.rs | 34 +++++++++++++++++++++++++--- 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/libs/hbb_common/protos/message.proto b/libs/hbb_common/protos/message.proto index 4554617a7d61..21f9e7aea0de 100644 --- a/libs/hbb_common/protos/message.proto +++ b/libs/hbb_common/protos/message.proto @@ -326,6 +326,7 @@ enum ClipboardFormat { ImageRgba = 21; ImagePng = 22; ImageSvg = 23; + Special = 31; } message Clipboard { @@ -334,6 +335,8 @@ message Clipboard { int32 width = 3; int32 height = 4; ClipboardFormat format = 5; + // Special format name, only used when format is Special. + string special_name = 6; } message MultiClipboards { repeated Clipboard clipboards = 1; } diff --git a/src/clipboard.rs b/src/clipboard.rs index 0510eca6a2d4..4e6295db9f24 100644 --- a/src/clipboard.rs +++ b/src/clipboard.rs @@ -12,6 +12,9 @@ pub const CLIPBOARD_INTERVAL: u64 = 333; // This format is used to store the flag in the clipboard. const RUSTDESK_CLIPBOARD_OWNER_FORMAT: &'static str = "dyn.com.rustdesk.owner"; +// Add special format for Excel XML Spreadsheet +const CLIPBOARD_FORMAT_EXCEL_XML_SPREADSHEET: &'static str = "XML Spreadsheet"; + lazy_static::lazy_static! { static ref ARBOARD_MTX: Arc> = Arc::new(Mutex::new(())); // cache the clipboard msg @@ -30,6 +33,7 @@ const SUPPORTED_FORMATS: &[ClipboardFormat] = &[ ClipboardFormat::ImageRgba, ClipboardFormat::ImagePng, ClipboardFormat::ImageSvg, + ClipboardFormat::Special(CLIPBOARD_FORMAT_EXCEL_XML_SPREADSHEET), ClipboardFormat::Special(RUSTDESK_CLIPBOARD_OWNER_FORMAT), ]; @@ -267,8 +271,8 @@ impl ClipboardContext { } if !force { for c in data.iter() { - if let ClipboardData::Special((_, d)) = c { - if side.is_owner(d) { + if let ClipboardData::Special((s, d)) = c { + if s == RUSTDESK_CLIPBOARD_OWNER_FORMAT && side.is_owner(d) { return Ok(vec![]); } } @@ -276,7 +280,10 @@ impl ClipboardContext { } Ok(data .into_iter() - .filter(|c| !matches!(c, ClipboardData::Special(_))) + .filter(|c| match c { + ClipboardData::Special((s, _)) => s != RUSTDESK_CLIPBOARD_OWNER_FORMAT, + _ => true, + }) .collect()) } @@ -454,12 +461,30 @@ mod proto { } } + fn special_to_proto(d: Vec, s: String) -> Clipboard { + let compressed = compress_func(&d); + let compress = compressed.len() < d.len(); + let content = if compress { + compressed + } else { + s.bytes().collect::>() + }; + Clipboard { + compress, + content: content.into(), + format: ClipboardFormat::Special.into(), + special_name: s, + ..Default::default() + } + } + fn clipboard_data_to_proto(data: ClipboardData) -> Option { let d = match data { ClipboardData::Text(s) => plain_to_proto(s, ClipboardFormat::Text), ClipboardData::Rtf(s) => plain_to_proto(s, ClipboardFormat::Rtf), ClipboardData::Html(s) => plain_to_proto(s, ClipboardFormat::Html), ClipboardData::Image(a) => image_to_proto(a), + ClipboardData::Special((s, d)) => special_to_proto(d, s), _ => return None, }; Some(d) @@ -496,6 +521,9 @@ mod proto { Ok(ClipboardFormat::ImageSvg) => Some(ClipboardData::Image(arboard::ImageData::svg( std::str::from_utf8(&data).unwrap_or_default(), ))), + Ok(ClipboardFormat::Special) => { + Some(ClipboardData::Special((clipboard.special_name, data))) + } _ => None, } } From e40243b55dc44ac7a0462dfbd748dc7540b237a1 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Wed, 4 Sep 2024 17:04:48 +0800 Subject: [PATCH 159/541] Fix/wf cliprdr c bugs (#9253) * fix: ResetEvent() after WaitForSingleObject() Signed-off-by: fufesou * fix: check and free mem Signed-off-by: fufesou --------- Signed-off-by: fufesou --- libs/clipboard/src/windows/wf_cliprdr.c | 241 +++++++++++++++++++----- 1 file changed, 190 insertions(+), 51 deletions(-) diff --git a/libs/clipboard/src/windows/wf_cliprdr.c b/libs/clipboard/src/windows/wf_cliprdr.c index 1f65b1913b2d..c2b7556a46ab 100644 --- a/libs/clipboard/src/windows/wf_cliprdr.c +++ b/libs/clipboard/src/windows/wf_cliprdr.c @@ -289,6 +289,9 @@ static BOOL try_open_clipboard(HWND hwnd) static HRESULT STDMETHODCALLTYPE CliprdrStream_QueryInterface(IStream *This, REFIID riid, void **ppvObject) { + if (ppvObject == NULL) + return E_INVALIDARG; + if (IsEqualIID(riid, &IID_IStream) || IsEqualIID(riid, &IID_IUnknown)) { IStream_AddRef(This); @@ -364,6 +367,13 @@ static HRESULT STDMETHODCALLTYPE CliprdrStream_Read(IStream *This, void *pv, ULO } *pcbRead = clipboard->req_fsize; + // Check overflow, can not be a real case + if ((instance->m_lOffset.QuadPart + clipboard->req_fsize) < instance->m_lOffset.QuadPart) { + // It's better to crash to release the explorer.exe + // This is a critical error, because the explorer is waiting for the data + // and the m_lOffset is wrong(overflowed) + return S_FALSE; + } instance->m_lOffset.QuadPart += clipboard->req_fsize; if (clipboard->req_fsize < cb) @@ -519,11 +529,17 @@ static HRESULT STDMETHODCALLTYPE CliprdrStream_Clone(IStream *This, IStream **pp static CliprdrStream *CliprdrStream_New(UINT32 connID, ULONG index, void *pData, const FILEDESCRIPTORW *dsc) { - IStream *iStream; + IStream *iStream = NULL; BOOL success = FALSE; BOOL isDir = FALSE; - CliprdrStream *instance; + CliprdrStream *instance = NULL; wfClipboard *clipboard = (wfClipboard *)pData; + + if (!(pData && dsc)) + { + return NULL; + } + instance = (CliprdrStream *)calloc(1, sizeof(CliprdrStream)); if (instance) @@ -876,14 +892,18 @@ static HRESULT STDMETHODCALLTYPE CliprdrDataObject_EnumDAdvise(IDataObject *This static CliprdrDataObject *CliprdrDataObject_New(UINT32 connID, FORMATETC *fmtetc, STGMEDIUM *stgmed, ULONG count, void *data) { - CliprdrDataObject *instance; - IDataObject *iDataObject; + CliprdrDataObject *instance = NULL; + IDataObject *iDataObject = NULL; instance = (CliprdrDataObject *)calloc(1, sizeof(CliprdrDataObject)); if (!instance) goto error; + instance->m_pFormatEtc = NULL; + instance->m_pStgMedium = NULL; + iDataObject = &instance->iDataObject; + iDataObject->lpVtbl = NULL; iDataObject->lpVtbl = (IDataObjectVtbl *)calloc(1, sizeof(IDataObjectVtbl)); if (!iDataObject->lpVtbl) @@ -931,7 +951,24 @@ static CliprdrDataObject *CliprdrDataObject_New(UINT32 connID, FORMATETC *fmtetc return instance; error: - CliprdrDataObject_Delete(instance); + if (iDataObject && iDataObject->lpVtbl) + { + free(iDataObject->lpVtbl); + } + if (instance) + { + if (instance->m_pFormatEtc) + { + free(instance->m_pFormatEtc); + } + + if (instance->m_pStgMedium) + { + free(instance->m_pStgMedium); + } + + CliprdrDataObject_Delete(instance); + } return NULL; } @@ -1012,6 +1049,8 @@ static HRESULT STDMETHODCALLTYPE CliprdrEnumFORMATETC_QueryInterface(IEnumFORMAT REFIID riid, void **ppvObject) { (void)This; + if (!ppvObject) + return E_INVALIDARG; if (IsEqualIID(riid, &IID_IEnumFORMATETC) || IsEqualIID(riid, &IID_IUnknown)) { @@ -1200,6 +1239,7 @@ static UINT32 get_local_format_id_by_name(wfClipboard *clipboard, const TCHAR *f WCHAR *unicode_name; #if !defined(UNICODE) size_t size; + int towchar_count; #endif if (!clipboard || !format_name) @@ -1207,6 +1247,8 @@ static UINT32 get_local_format_id_by_name(wfClipboard *clipboard, const TCHAR *f #if defined(UNICODE) unicode_name = _wcsdup(format_name); + if (!unicode_name) + return 0; #else size = _tcslen(format_name); unicode_name = calloc(size + 1, sizeof(WCHAR)); @@ -1214,11 +1256,13 @@ static UINT32 get_local_format_id_by_name(wfClipboard *clipboard, const TCHAR *f if (!unicode_name) return 0; - MultiByteToWideChar(CP_OEMCP, 0, format_name, strlen(format_name), unicode_name, size); -#endif - - if (!unicode_name) + towchar_count = MultiByteToWideChar(CP_OEMCP, 0, format_name, strlen(format_name), NULL, 0); + if (towchar_count <= 0 || towchar_count > size) + return 0; + towchar_count = MultiByteToWideChar(CP_OEMCP, 0, format_name, strlen(format_name), unicode_name, size); + if (towchar_count <= 0) return 0; +#endif for (i = 0; i < clipboard->map_size; i++) { @@ -1314,6 +1358,9 @@ static UINT cliprdr_send_tempdir(wfClipboard *clipboard) if (!clipboard) return -1; + // to-do: + // Directly use the environment variable `TEMP` is not safe. + // But this function is not used for now. if (GetEnvironmentVariableA("TEMP", tempDirectory.szTempDir, sizeof(tempDirectory.szTempDir)) == 0) return -1; @@ -1446,6 +1493,36 @@ static UINT cliprdr_send_format_list(wfClipboard *clipboard, UINT32 connID) return rc; } +// Ensure the event is not signaled, and reset it if it is. +UINT try_reset_event(HANDLE event) +{ + if (!event) + { + return ERROR_INTERNAL_ERROR; + } + + DWORD result = WaitForSingleObject(event, 0); + if (result == WAIT_OBJECT_0) + { + if (!ResetEvent(event)) + { + return GetLastError(); + } + else + { + return ERROR_SUCCESS; + } + } + else if (result == WAIT_TIMEOUT) + { + return ERROR_SUCCESS; + } + else + { + return ERROR_INTERNAL_ERROR; + } +} + UINT wait_response_event(UINT32 connID, wfClipboard *clipboard, HANDLE event, BOOL* recvedFlag, void **data) { UINT rc = ERROR_SUCCESS; @@ -1470,6 +1547,11 @@ UINT wait_response_event(UINT32 connID, wfClipboard *clipboard, HANDLE event, BO } } + if (!ResetEvent(event)) + { + // NOTE: critical error here, crash may be better + } + if (clipboard->context->IsStopped == TRUE) { wf_do_empty_cliprdr(clipboard); @@ -1481,12 +1563,6 @@ UINT wait_response_event(UINT32 connID, wfClipboard *clipboard, HANDLE event, BO return ERROR_INTERNAL_ERROR; } - if (!ResetEvent(event)) - { - // NOTE: critical error here, crash may be better - rc = ERROR_INTERNAL_ERROR; - } - if ((*data) == NULL) { rc = ERROR_INTERNAL_ERROR; @@ -1530,12 +1606,18 @@ static UINT cliprdr_send_data_request(UINT32 connID, wfClipboard *clipboard, UIN if (!clipboard || !clipboard->context || !clipboard->context->ClientFormatDataRequest) return ERROR_INTERNAL_ERROR; + rc = try_reset_event(clipboard->formatDataRespEvent); + if (rc != ERROR_SUCCESS) + { + return rc; + } + clipboard->formatDataRespReceived = FALSE; + remoteFormatId = get_remote_format_id(clipboard, formatId); formatDataRequest.connID = connID; formatDataRequest.requestedFormatId = remoteFormatId; clipboard->requestedFormatId = formatId; - clipboard->formatDataRespReceived = FALSE; rc = clipboard->context->ClientFormatDataRequest(clipboard->context, &formatDataRequest); if (rc != ERROR_SUCCESS) { @@ -1555,7 +1637,17 @@ UINT cliprdr_send_request_filecontents(wfClipboard *clipboard, UINT32 connID, co if (!clipboard || !clipboard->context || !clipboard->context->ClientFileContentsRequest) return ERROR_INTERNAL_ERROR; + rc = try_reset_event(clipboard->req_fevent); + if (rc != ERROR_SUCCESS) + { + return rc; + } + clipboard->req_f_received = FALSE; + fileContentsRequest.connID = connID; + // streamId is `IStream*` pointer, though it is not very good on a 64-bit system. + // But it is OK, because it is only used to check if the stream is the same in + // `wf_cliprdr_server_file_contents_request()` function. fileContentsRequest.streamId = (UINT32)(ULONG_PTR)streamid; fileContentsRequest.listIndex = index; fileContentsRequest.dwFlags = flag; @@ -1564,7 +1656,6 @@ UINT cliprdr_send_request_filecontents(wfClipboard *clipboard, UINT32 connID, co fileContentsRequest.cbRequested = nreq; fileContentsRequest.clipDataId = 0; fileContentsRequest.msgFlags = 0; - clipboard->req_f_received = FALSE; rc = clipboard->context->ClientFileContentsRequest(clipboard->context, &fileContentsRequest); if (rc != ERROR_SUCCESS) { @@ -1801,6 +1892,7 @@ static LRESULT CALLBACK cliprdr_proc(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM break; case WM_DESTROYCLIPBOARD: + // to-do: clear clipboard data? case WM_ASKCBFORMATNAME: case WM_HSCROLLCLIPBOARD: case WM_PAINTCLIPBOARD: @@ -1917,7 +2009,7 @@ static BOOL wf_cliprdr_get_file_contents(WCHAR *file_name, BYTE *buffer, LONG po LONG positionHigh, DWORD nRequested, DWORD *puSize) { BOOL res = FALSE; - HANDLE hFile; + HANDLE hFile = NULL; DWORD nGet, rc; if (!file_name || !buffer || !puSize) @@ -1945,9 +2037,11 @@ static BOOL wf_cliprdr_get_file_contents(WCHAR *file_name, BYTE *buffer, LONG po res = TRUE; error: - - if (!CloseHandle(hFile)) - res = FALSE; + if (hFile) + { + if (!CloseHandle(hFile)) + res = FALSE; + } if (res) *puSize = nGet; @@ -1958,8 +2052,8 @@ static BOOL wf_cliprdr_get_file_contents(WCHAR *file_name, BYTE *buffer, LONG po /* path_name has a '\' at the end. e.g. c:\newfolder\, file_name is c:\newfolder\new.txt */ static FILEDESCRIPTORW *wf_cliprdr_get_file_descriptor(WCHAR *file_name, size_t pathLen) { - HANDLE hFile; - FILEDESCRIPTORW *fd; + HANDLE hFile = NULL; + FILEDESCRIPTORW *fd = NULL; fd = (FILEDESCRIPTORW *)calloc(1, sizeof(FILEDESCRIPTORW)); if (!fd) @@ -1988,7 +2082,16 @@ static FILEDESCRIPTORW *wf_cliprdr_get_file_descriptor(WCHAR *file_name, size_t } fd->nFileSizeLow = GetFileSize(hFile, &fd->nFileSizeHigh); - wcscpy_s(fd->cFileName, sizeof(fd->cFileName) / 2, file_name + pathLen); + if ((wcslen(file_name + pathLen) + 1) > sizeof(fd->cFileName) / sizeof(fd->cFileName[0])) + { + // The file name is too long, which is not a normal case. + // So we just return NULL. + CloseHandle(hFile); + free(fd); + return NULL; + } + + wcsncpy_s(fd->cFileName, sizeof(fd->cFileName) / sizeof(fd->cFileName[0]), file_name + pathLen, wcslen(file_name + pathLen) + 1); CloseHandle(hFile); return fd; @@ -2037,7 +2140,12 @@ static BOOL wf_cliprdr_add_to_file_arrays(wfClipboard *clipboard, WCHAR *full_fi if (!clipboard->file_names[clipboard->nFiles]) return FALSE; - wcscpy_s(clipboard->file_names[clipboard->nFiles], MAX_PATH, full_file_name); + // `MAX_PATH` is long enough for the file name. + // So we just return FALSE if the file name is too long, which is not a normal case. + if ((wcslen(full_file_name) + 1) > MAX_PATH) + return FALSE; + + wcsncpy_s(clipboard->file_names[clipboard->nFiles], MAX_PATH, full_file_name, wcslen(full_file_name) + 1); /* add to descriptor array */ clipboard->fileDescriptor[clipboard->nFiles] = wf_cliprdr_get_file_descriptor(full_file_name, pathLen); @@ -2061,8 +2169,8 @@ static BOOL wf_cliprdr_traverse_directory(wfClipboard *clipboard, WCHAR *Dir, si if (!clipboard || !Dir) return FALSE; - // StringCchCopy(DirSpec, MAX_PATH, Dir); - // StringCchCat(DirSpec, MAX_PATH, TEXT("\\*")); + if (wcslen(Dir) + 3 > MAX_PATH) + return FALSE; StringCchCopyW(DirSpec, MAX_PATH, Dir); StringCchCatW(DirSpec, MAX_PATH, L"\\*"); @@ -2091,9 +2199,8 @@ static BOOL wf_cliprdr_traverse_directory(wfClipboard *clipboard, WCHAR *Dir, si if ((FindFileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0) { WCHAR DirAdd[MAX_PATH]; - // StringCchCopy(DirAdd, MAX_PATH, Dir); - // StringCchCat(DirAdd, MAX_PATH, _T("\\")); - // StringCchCat(DirAdd, MAX_PATH, FindFileData.cFileName); + if (wcslen(Dir) + wcslen(FindFileData.cFileName) + 2 > MAX_PATH) + return FALSE; StringCchCopyW(DirAdd, MAX_PATH, Dir); StringCchCatW(DirAdd, MAX_PATH, L"\\"); StringCchCatW(DirAdd, MAX_PATH, FindFileData.cFileName); @@ -2107,10 +2214,8 @@ static BOOL wf_cliprdr_traverse_directory(wfClipboard *clipboard, WCHAR *Dir, si else { WCHAR fileName[MAX_PATH]; - // StringCchCopy(fileName, MAX_PATH, Dir); - // StringCchCat(fileName, MAX_PATH, _T("\\")); - // StringCchCat(fileName, MAX_PATH, FindFileData.cFileName); - + if (wcslen(Dir) + wcslen(FindFileData.cFileName) + 2 > MAX_PATH) + return FALSE; StringCchCopyW(fileName, MAX_PATH, Dir); StringCchCatW(fileName, MAX_PATH, L"\\"); StringCchCatW(fileName, MAX_PATH, FindFileData.cFileName); @@ -2255,9 +2360,11 @@ static UINT wf_cliprdr_server_format_list(CliprdrClientContext *context, if (context->EnableFiles) { UINT32 *p_conn_id = (UINT32 *)calloc(1, sizeof(UINT32)); - *p_conn_id = formatList->connID; - if (PostMessage(clipboard->hwnd, WM_CLIPRDR_MESSAGE, OLE_SETCLIPBOARD, p_conn_id)) - rc = CHANNEL_RC_OK; + if (p_conn_id) { + *p_conn_id = formatList->connID; + if (PostMessage(clipboard->hwnd, WM_CLIPRDR_MESSAGE, OLE_SETCLIPBOARD, p_conn_id)) + rc = CHANNEL_RC_OK; + } } else { @@ -2278,16 +2385,30 @@ static UINT wf_cliprdr_server_format_list(CliprdrClientContext *context, // SetClipboardData(clipboard->format_mappings[i].local_format_id, NULL); FORMAT_IDS *format_ids = (FORMAT_IDS *)calloc(1, sizeof(FORMAT_IDS)); - format_ids->connID = formatList->connID; - format_ids->size = (UINT32)clipboard->map_size; - format_ids->formats = (UINT32 *)calloc(format_ids->size, sizeof(UINT32)); - for (i = 0; i < format_ids->size; ++i) + if (format_ids) { - format_ids->formats[i] = clipboard->format_mappings[i].local_format_id; - } - if (PostMessage(clipboard->hwnd, WM_CLIPRDR_MESSAGE, DELAYED_RENDERING, format_ids)) - { - rc = CHANNEL_RC_OK; + format_ids->connID = formatList->connID; + format_ids->size = (UINT32)clipboard->map_size; + format_ids->formats = (UINT32 *)calloc(format_ids->size, sizeof(UINT32)); + if (format_ids->formats) + { + for (i = 0; i < format_ids->size; ++i) + { + format_ids->formats[i] = clipboard->format_mappings[i].local_format_id; + } + if (PostMessage(clipboard->hwnd, WM_CLIPRDR_MESSAGE, DELAYED_RENDERING, format_ids)) + { + rc = CHANNEL_RC_OK; + } + else + { + rc = ERROR_INTERNAL_ERROR; + } + } + else + { + rc = ERROR_INTERNAL_ERROR; + } } else { @@ -2482,17 +2603,28 @@ wf_cliprdr_server_format_data_request(CliprdrClientContext *context, p += len + 1, clipboard->nFiles++) { int cchWideChar; - WCHAR *wFileName; cchWideChar = MultiByteToWideChar(CP_ACP, MB_COMPOSITE, p, len, NULL, 0); wFileName = (LPWSTR)calloc(cchWideChar, sizeof(WCHAR)); - MultiByteToWideChar(CP_ACP, MB_COMPOSITE, p, len, wFileName, cchWideChar); - wf_cliprdr_process_filename(clipboard, wFileName, cchWideChar); + if (wFileName) + { + MultiByteToWideChar(CP_ACP, MB_COMPOSITE, p, len, wFileName, cchWideChar); + wf_cliprdr_process_filename(clipboard, wFileName, cchWideChar); + free(wFileName); + } + else + { + rc = ERROR_INTERNAL_ERROR; + GlobalUnlock(stg_medium.hGlobal); + ReleaseStgMedium(&stg_medium); + goto exit; + } } } GlobalUnlock(stg_medium.hGlobal); ReleaseStgMedium(&stg_medium); resp: + // size will not overflow, because size type is size_t (unsigned __int64) size = 4 + clipboard->nFiles * sizeof(FILEDESCRIPTORW); groupDsc = (FILEGROUPDESCRIPTORW *)malloc(size); @@ -2532,10 +2664,17 @@ wf_cliprdr_server_format_data_request(CliprdrClientContext *context, globlemem = (char *)GlobalLock(hClipdata); size = (int)GlobalSize(hClipdata); buff = malloc(size); - CopyMemory(buff, globlemem, size); + if (buff) + { + CopyMemory(buff, globlemem, size); + rc = ERROR_SUCCESS; + } + else + { + rc = ERROR_INTERNAL_ERROR; + } GlobalUnlock(hClipdata); CloseClipboard(); - rc = ERROR_SUCCESS; } } else From 5f2901686175f76f6849eef2e76e51e36eee6a1c Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Wed, 4 Sep 2024 22:27:52 +0800 Subject: [PATCH 160/541] fix: build web (#9259) 1. Web, build. 2. Web and mobile, `onSubmitted` for ID text field. 3. Web, remove unused key 'toggle_option'. Signed-off-by: fufesou --- flutter/lib/mobile/pages/connection_page.dart | 3 +++ flutter/lib/mobile/pages/home_page.dart | 3 ++- flutter/lib/web/bridge.dart | 8 ++++++-- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/flutter/lib/mobile/pages/connection_page.dart b/flutter/lib/mobile/pages/connection_page.dart index 9fcef8e3f14b..c6e812389264 100644 --- a/flutter/lib/mobile/pages/connection_page.dart +++ b/flutter/lib/mobile/pages/connection_page.dart @@ -249,6 +249,9 @@ class _ConnectionPageState extends State { ), ), inputFormatters: [IDTextInputFormatter()], + onSubmitted: (_) { + onConnect(); + }, ); }, onSelected: (option) { diff --git a/flutter/lib/mobile/pages/home_page.dart b/flutter/lib/mobile/pages/home_page.dart index 0db7a2b91e3a..d26d91685420 100644 --- a/flutter/lib/mobile/pages/home_page.dart +++ b/flutter/lib/mobile/pages/home_page.dart @@ -46,10 +46,11 @@ class HomePageState extends State { void initPages() { _pages.clear(); - if (!bind.isIncomingOnly()) + if (!bind.isIncomingOnly()) { _pages.add(ConnectionPage( appBarActions: [], )); + } if (isAndroid && !bind.isOutgoingOnly()) { _chatPageTabIndex = _pages.length; _pages.addAll([ChatPage(type: ChatPageType.mobileMain), ServerPage()]); diff --git a/flutter/lib/web/bridge.dart b/flutter/lib/web/bridge.dart index 8805831bc53b..d1b777dd1e4d 100644 --- a/flutter/lib/web/bridge.dart +++ b/flutter/lib/web/bridge.dart @@ -187,7 +187,7 @@ class RustdeskImpl { Future sessionToggleOption( {required UuidValue sessionId, required String value, dynamic hint}) { return Future( - () => js.context.callMethod('setByName', ['toggle_option', value])); + () => js.context.callMethod('setByName', ['option:toggle', value])); } Future sessionTogglePrivacyMode( @@ -196,7 +196,7 @@ class RustdeskImpl { required bool on, dynamic hint}) { return Future(() => js.context.callMethod('setByName', [ - 'toggle_option', + 'option:toggle', jsonEncode({implKey, on}) ])); } @@ -1704,6 +1704,10 @@ class RustdeskImpl { throw UnimplementedError(); } + int mainMaxEncryptLen({dynamic hint}) { + throw UnimplementedError(); + } + sessionRenameFile( {required UuidValue sessionId, required int actId, From 3bd34bf0b9a9073105db084b01d4b0a108affeab Mon Sep 17 00:00:00 2001 From: 21pages Date: Thu, 5 Sep 2024 18:34:48 +0800 Subject: [PATCH 161/541] increase interval for restart linux ui, try fix loop start (#9264) Signed-off-by: 21pages --- src/ipc.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ipc.rs b/src/ipc.rs index 6d5e247d42ce..2b591d107110 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -401,7 +401,8 @@ async fn handle(data: Data, stream: &mut Connection) { std::fs::remove_file(&Config::ipc_path("")).ok(); #[cfg(target_os = "linux")] { - hbb_common::sleep((crate::platform::SERVICE_INTERVAL * 2) as f32 / 1000.0) + // https://github.com/rustdesk/rustdesk/discussions/9254, slow on some machines + hbb_common::sleep((crate::platform::SERVICE_INTERVAL * 2) as f32 / 1000.0 + 1.2) .await; crate::run_me::<&str>(vec![]).ok(); } From 7a1157f1b0bb524f5d9595830e1595ec24dffb52 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Thu, 5 Sep 2024 22:37:14 +0800 Subject: [PATCH 162/541] refact: quality status event (#9268) Signed-off-by: fufesou --- flutter/lib/common/widgets/overlay.dart | 3 ++- flutter/lib/models/model.dart | 18 ++++++++++++------ 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/flutter/lib/common/widgets/overlay.dart b/flutter/lib/common/widgets/overlay.dart index 9b20136e1505..a1620b106e36 100644 --- a/flutter/lib/common/widgets/overlay.dart +++ b/flutter/lib/common/widgets/overlay.dart @@ -595,7 +595,8 @@ class QualityMonitor extends StatelessWidget { "${qualityMonitorModel.data.targetBitrate ?? '-'}kb"), _row( "Codec", qualityMonitorModel.data.codecFormat ?? '-'), - _row("Chroma", qualityMonitorModel.data.chroma ?? '-'), + if (!isWeb) + _row("Chroma", qualityMonitorModel.data.chroma ?? '-'), ], ), ) diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 6f2a9eb2e2d1..c622d9f10d88 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -2230,8 +2230,10 @@ class QualityMonitorModel with ChangeNotifier { updateQualityStatus(Map evt) { try { - if ((evt['speed'] as String).isNotEmpty) _data.speed = evt['speed']; - if ((evt['fps'] as String).isNotEmpty) { + if (evt.containsKey('speed') && (evt['speed'] as String).isNotEmpty) { + _data.speed = evt['speed']; + } + if (evt.containsKey('fps') && (evt['fps'] as String).isNotEmpty) { final fps = jsonDecode(evt['fps']) as Map; final pi = parent.target?.ffiModel.pi; if (pi != null) { @@ -2252,14 +2254,18 @@ class QualityMonitorModel with ChangeNotifier { _data.fps = null; } } - if ((evt['delay'] as String).isNotEmpty) _data.delay = evt['delay']; - if ((evt['target_bitrate'] as String).isNotEmpty) { + if (evt.containsKey('delay') && (evt['delay'] as String).isNotEmpty) { + _data.delay = evt['delay']; + } + if (evt.containsKey('target_bitrate') && + (evt['target_bitrate'] as String).isNotEmpty) { _data.targetBitrate = evt['target_bitrate']; } - if ((evt['codec_format'] as String).isNotEmpty) { + if (evt.containsKey('codec_format') && + (evt['codec_format'] as String).isNotEmpty) { _data.codecFormat = evt['codec_format']; } - if ((evt['chroma'] as String).isNotEmpty) { + if (evt.containsKey('chroma') && (evt['chroma'] as String).isNotEmpty) { _data.chroma = evt['chroma']; } notifyListeners(); From 415003658912927564ff16fcb27156410b336586 Mon Sep 17 00:00:00 2001 From: 21pages Date: Thu, 5 Sep 2024 22:48:20 +0800 Subject: [PATCH 163/541] remove first frame fallback if repeat (#9267) Signed-off-by: 21pages --- src/server/video_service.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/server/video_service.rs b/src/server/video_service.rs index e091966034be..aeff1911e017 100644 --- a/src/server/video_service.rs +++ b/src/server/video_service.rs @@ -947,7 +947,9 @@ fn handle_one_frame( } else { 3 }; - if first || *encode_fail_counter >= max_fail_times { + let repeat = !encoder.latency_free(); + // repeat encoders can reach max_fail_times on the first frame + if (first && !repeat) || *encode_fail_counter >= max_fail_times { *encode_fail_counter = 0; if encoder.is_hardware() { encoder.disable(); From 26ebd0deb92e630511fe4ee65a66b1c4e77b20fe Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Thu, 5 Sep 2024 23:39:07 +0800 Subject: [PATCH 164/541] fix: clipboard, cmd ipc (#9270) 1. Send raw contents if `content_len` > 1024*3. 2. Send raw contents if it is not empty. 3. Try read clipboard again if no data from cm. Signed-off-by: fufesou --- src/server/clipboard_service.rs | 43 ++++++++++++++++++--------------- src/ui_cm_interface.rs | 8 +++--- 2 files changed, 29 insertions(+), 22 deletions(-) diff --git a/src/server/clipboard_service.rs b/src/server/clipboard_service.rs index 3040a8f88f33..e70974258575 100644 --- a/src/server/clipboard_service.rs +++ b/src/server/clipboard_service.rs @@ -95,25 +95,30 @@ impl Handler { log::error!("Failed to read clipboard from cm: {}", e); } Ok(data) => { - let mut msg = Message::new(); - let multi_clipboards = MultiClipboards { - clipboards: data - .into_iter() - .map(|c| Clipboard { - compress: c.compress, - content: c.content, - width: c.width, - height: c.height, - format: ClipboardFormat::from_i32(c.format) - .unwrap_or(ClipboardFormat::Text) - .into(), - ..Default::default() - }) - .collect(), - ..Default::default() - }; - msg.set_multi_clipboards(multi_clipboards); - return Some(msg); + // Skip sending empty clipboard data. + // Maybe there's something wrong reading the clipboard data in cm, but no error msg is returned. + // The clipboard data should not be empty, the last line will try again to get the clipboard data. + if !data.is_empty() { + let mut msg = Message::new(); + let multi_clipboards = MultiClipboards { + clipboards: data + .into_iter() + .map(|c| Clipboard { + compress: c.compress, + content: c.content, + width: c.width, + height: c.height, + format: ClipboardFormat::from_i32(c.format) + .unwrap_or(ClipboardFormat::Text) + .into(), + ..Default::default() + }) + .collect(), + ..Default::default() + }; + msg.set_multi_clipboards(multi_clipboards); + return Some(msg); + } } } } diff --git a/src/ui_cm_interface.rs b/src/ui_cm_interface.rs index 89e9ceabbeae..f1748112ea6b 100644 --- a/src/ui_cm_interface.rs +++ b/src/ui_cm_interface.rs @@ -498,10 +498,10 @@ impl IpcTaskRunner { let (content, next_raw) = { // TODO: find out a better threshold if content_len > 1024 * 3 { - (c.content, false) - } else { raw_contents.extend(c.content); (bytes::Bytes::new(), true) + } else { + (c.content, false) } }; main_data.push(ClipboardNonFile { @@ -515,7 +515,9 @@ impl IpcTaskRunner { }); } allow_err!(self.stream.send(&Data::ClipboardNonFile(Some(("".to_owned(), main_data)))).await); - allow_err!(self.stream.send_raw(raw_contents.into()).await); + if !raw_contents.is_empty() { + allow_err!(self.stream.send_raw(raw_contents.into()).await); + } } Err(e) => { allow_err!(self.stream.send(&Data::ClipboardNonFile(Some((format!("{}", e), vec![])))).await); From aa3402b44a9acff175ad18b8a898d265fe8990a1 Mon Sep 17 00:00:00 2001 From: Xp96 <38923106+Xp96@users.noreply.github.com> Date: Thu, 5 Sep 2024 14:15:07 -0300 Subject: [PATCH 165/541] Update ptbr.rs (#9271) --- src/lang/ptbr.rs | 190 +++++++++++++++++++++++------------------------ 1 file changed, 95 insertions(+), 95 deletions(-) diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index 83ee1e0d2339..a5ba6de7b0fa 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -498,131 +498,131 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("resolution_original_tip", "Resolução original"), ("resolution_fit_local_tip", "Adequar a resolução local"), ("resolution_custom_tip", "Customizar resolução"), - ("Collapse toolbar", ""), - ("Accept and Elevate", ""), - ("accept_and_elevate_btn_tooltip", ""), - ("clipboard_wait_response_timeout_tip", ""), - ("Incoming connection", ""), - ("Outgoing connection", ""), + ("Collapse toolbar", "Ocultar barra de ferramentas"), + ("Accept and Elevate", "Aceitar e elevar"), + ("accept_and_elevate_btn_tooltip", "Aceitar a conexão e elevar os privilégios do UAC."), + ("clipboard_wait_response_timeout_tip", "Tempo de espera para a resposta da área de transferência expirado."), + ("Incoming connection", "Conexão de entrada"), + ("Outgoing connection", "Conexão de saída"), ("Exit", "Sair"), ("Open", "Abrir"), - ("logout_tip", ""), + ("logout_tip", "Tem certeza que deseja sair?"), ("Service", "Serviço"), ("Start", "Iniciar"), ("Stop", "Parar"), - ("exceed_max_devices", ""), + ("exceed_max_devices", "Você atingiu o número máximo de dispositivos gerenciados."), ("Sync with recent sessions", "Sincronizar com sessões recentes"), ("Sort tags", "Classificar tags"), ("Open connection in new tab", "Abrir conexão em uma nova aba"), ("Move tab to new window", "Mover aba para uma nova janela"), ("Can not be empty", "Não pode estar vazio"), - ("Already exists", ""), + ("Already exists", "Já existe"), ("Change Password", "Alterar senha"), ("Refresh Password", "Atualizar senha"), - ("ID", ""), - ("Grid View", ""), - ("List View", ""), - ("Select", ""), - ("Toggle Tags", ""), - ("pull_ab_failed_tip", ""), - ("push_ab_failed_tip", ""), - ("synced_peer_readded_tip", ""), + ("ID", "ID"), + ("Grid View", "Visualização em grade"), + ("List View", "Visualização em lista"), + ("Select", "Selecionar"), + ("Toggle Tags", "Alternar etiquetas"), + ("pull_ab_failed_tip", "Não foi possível atualizar o diretório"), + ("push_ab_failed_tip", "Não foi possível sincronizar o diretório com o servidor"), + ("synced_peer_readded_tip", "Os dispositivos presentes em sessões recentes serão sincronizados com o diretório."), ("Change Color", "Alterar cor"), ("Primary Color", "Cor principal"), - ("HSV Color", ""), - ("Installation Successful!", ""), - ("Installation failed!", ""), - ("Reverse mouse wheel", ""), + ("HSV Color", "Cor HSV"), + ("Installation Successful!", "Instalação bem-sucedida!"), + ("Installation failed!", "A instalação falhou!"), + ("Reverse mouse wheel", "Inverter rolagem do mouse"), ("{} sessions", ""), - ("scam_title", ""), - ("scam_text1", ""), - ("scam_text2", ""), - ("Don't show again", ""), + ("scam_title", "Você pode estar sendo ENGANADO!"), + ("scam_text1", "Se você estiver ao telefone com alguém que NÃO conhece e em quem NÃO confia e essa pessoa pedir para você usar o RustDesk e iniciar o serviço, NÃO faça isso !! e desligue imediatamente."), + ("scam_text2", "Provavelmente são golpistas tentando roubar seu dinheiro ou informações privadas."), + ("Don't show again", "Não mostrar novamente"), ("I Agree", "Eu concordo"), - ("Decline", ""), - ("Timeout in minutes", ""), - ("auto_disconnect_option_tip", ""), - ("Connection failed due to inactivity", ""), - ("Check for software update on startup", ""), - ("upgrade_rustdesk_server_pro_to_{}_tip", ""), - ("pull_group_failed_tip", ""), + ("Decline", "Recusar"), + ("Timeout in minutes", "Tempo limite em minutos"), + ("auto_disconnect_option_tip", "Encerrar sessões entrantes automaticamente por inatividade do usuário."), + ("Connection failed due to inactivity", "Conexão encerrada automaticamente por inatividade."), + ("Check for software update on startup", "Verificar atualizações do software ao iniciar"), + ("upgrade_rustdesk_server_pro_to_{}_tip", "Atualize o RustDesk Server Pro para a versão {} ou superior."), + ("pull_group_failed_tip", "Não foi possível atualizar o grupo."), ("Filter by intersection", ""), ("Remove wallpaper during incoming sessions", "Remover papel de parede durante sessão remota"), ("Test", "Teste"), - ("display_is_plugged_out_msg", ""), - ("No displays", ""), - ("Open in new window", ""), - ("Show displays as individual windows", ""), - ("Use all my displays for the remote session", ""), - ("selinux_tip", ""), + ("display_is_plugged_out_msg", "A tela está desconectada. Mudando para a principal."), + ("No displays", "Nenhum display encontrado"), + ("Open in new window", "Abrir em uma nova janela"), + ("Show displays as individual windows", "Mostrar as telas como janelas individuais"), + ("Use all my displays for the remote session", "Usar todas as minhas telas para a sessão remota"), + ("selinux_tip", "O SELinux está ativado em seu dispositivo, o que pode impedir que o RustDesk funcione corretamente como dispositivo controlado."), ("Change view", "Alterar visualização"), - ("Big tiles", ""), - ("Small tiles", ""), + ("Big tiles", "Ícones grandes"), + ("Small tiles", "Ícones pequenos"), ("List", "Lista"), ("Virtual display", "Display Virtual"), - ("Plug out all", ""), - ("True color (4:4:4)", ""), - ("Enable blocking user input", ""), - ("id_input_tip", ""), + ("Plug out all", "Desconectar tudo"), + ("True color (4:4:4)", "Cor verdadeira (4:4:4)"), + ("Enable blocking user input", "Habilitar bloqueio da entrada do usuário"), + ("id_input_tip", "Você pode inserir um ID, um IP direto ou um domínio com uma porta (:).\nPara acessar um dispositivo em outro servidor, adicione o IP do servidor (@?key=), por exemplo,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nPara acessar um dispositivo em um servidor público, insira \"@public\", a chave não é necessária para um servidor público."), ("privacy_mode_impl_mag_tip", ""), ("privacy_mode_impl_virtual_display_tip", ""), ("Enter privacy mode", "Entrar no modo privado"), ("Exit privacy mode", "Sair do modo privado"), - ("idd_not_support_under_win10_2004_tip", ""), - ("input_source_1_tip", ""), - ("input_source_2_tip", ""), - ("Swap control-command key", ""), - ("swap-left-right-mouse", ""), + ("idd_not_support_under_win10_2004_tip", "O driver de tela indireto não é suportado. É necessário o Windows 10, versão 2004 ou superior."), + ("input_source_1_tip", "Fonte de entrada 1"), + ("input_source_2_tip", "Fonte de entrada 2"), + ("Swap control-command key", "Trocar teclas Control e Comando"), + ("swap-left-right-mouse", "Trocar botões esquerdo e direito do mouse"), ("2FA code", "Código 2FA"), - ("More", ""), - ("enable-2fa-title", ""), - ("enable-2fa-desc", ""), - ("wrong-2fa-code", ""), - ("enter-2fa-title", ""), - ("Email verification code must be 6 characters.", ""), - ("2FA code must be 6 digits.", ""), - ("Multiple Windows sessions found", ""), + ("More", "Mais"), + ("enable-2fa-title", "Habilitar autenticação em duas etapas"), + ("enable-2fa-desc", "Configure seu autenticador agora. Você pode usar um aplicativo de autenticação como Authy, Microsoft ou Google Authenticator em seu telefone ou computador. Escaneie o código QR com seu aplicativo e insira o código mostrado para habilitar a autenticação em duas etapas."), + ("wrong-2fa-code", "Código inválido. Verifique se o código e as configurações de horário estão corretas."), + ("enter-2fa-title", "Autenticação em duas etapas"), + ("Email verification code must be 6 characters.", "O código de verificação por e-mail deve ter 6 caracteres."), + ("2FA code must be 6 digits.", "O código 2FA deve ter 6 dígitos."), + ("Multiple Windows sessions found", "Múltiplas sessões de janela encontradas"), ("Please select the session you want to connect to", "Por favor, selecione a sessão que você deseja se conectar"), - ("powered_by_me", ""), - ("outgoing_only_desk_tip", ""), - ("preset_password_warning", ""), - ("Security Alert", ""), - ("My address book", ""), + ("powered_by_me", "Desenvolvido por RustDesk"), + ("outgoing_only_desk_tip", "Esta é uma edição personalizada.\nVocê pode se conectar a outros dispositivos, mas eles não podem se conectar ao seu."), + ("preset_password_warning", "Atenção: esta edição personalizada vem com uma senha predefinida. Qualquer pessoa que a conhecer poderá controlar totalmente seu dispositivo. Se isso não for o que você deseja, desinstale o software imediatamente."), + ("Security Alert", "Alerta de Segurança"), + ("My address book", "Minha lista de contatos"), ("Personal", ""), - ("Owner", ""), + ("Owner", "Proprietário"), ("Set shared password", "Definir senha compartilhada"), ("Exist in", ""), ("Read-only", "Apenas leitura"), ("Read/Write", "Leitura/escrita"), ("Full Control", "Controle total"), - ("share_warning_tip", ""), + ("share_warning_tip", "Os campos mostrados acima são compartilhados e visíveis por outras pessoas."), ("Everyone", "Todos"), - ("ab_web_console_tip", ""), - ("allow-only-conn-window-open-tip", ""), - ("no_need_privacy_mode_no_physical_displays_tip", ""), - ("Follow remote cursor", ""), - ("Follow remote window focus", ""), + ("ab_web_console_tip", "Mais opções no console web"), + ("allow-only-conn-window-open-tip", "Permitir conexões apenas quando a janela do RustDesk estiver aberta"), + ("no_need_privacy_mode_no_physical_displays_tip", "Sem telas físicas, o modo privado não é necessário"), + ("Follow remote cursor", "Seguir cursor remoto"), + ("Follow remote window focus", "Seguir janela remota ativa"), ("default_proxy_tip", ""), - ("no_audio_input_device_tip", ""), + ("no_audio_input_device_tip", "Nenhum dispositivo de entrada de áudio encontrado"), ("Incoming", ""), ("Outgoing", ""), - ("Clear Wayland screen selection", ""), - ("clear_Wayland_screen_selection_tip", ""), - ("confirm_clear_Wayland_screen_selection_tip", ""), + ("Clear Wayland screen selection", "Limpar seleção de tela do Wayland"), + ("clear_Wayland_screen_selection_tip", "Depois de limpar a seleção de tela, você pode selecioná-la novamente para compartilhar."), + ("confirm_clear_Wayland_screen_selection_tip", "Tem certeza que deseja limpar a seleção da tela do Wayland?"), ("android_new_voice_call_tip", ""), - ("texture_render_tip", ""), - ("Use texture rendering", ""), - ("Floating window", ""), - ("floating_window_tip", ""), - ("Keep screen on", ""), - ("Never", ""), - ("During controlled", ""), - ("During service is on", ""), - ("Capture screen using DirectX", ""), + ("texture_render_tip", "Use renderização de textura para tornar as imagens mais suaves"), + ("Use texture rendering", "Usar renderização de textura"), + ("Floating window", "Janela flutuante"), + ("floating_window_tip", "Ajuda a manter o serviço RustDesk em segundo plano"), + ("Keep screen on", "Manter tela ligada"), + ("Never", "Nunca"), + ("During controlled", "Durante controle"), + ("During service is on", "Enquanto o serviço estiver ativo"), + ("Capture screen using DirectX", "Capturar tela usando DirectX"), ("Back", "Voltar"), - ("Apps", ""), - ("Volume up", ""), - ("Volume down", ""), + ("Apps", "Apps"), + ("Volume up", "Aumentar volume"), + ("Volume down", "Diminuir volume"), ("Power", ""), ("Telegram bot", "Bot Telegram"), ("enable-bot-tip", "Se você ativar este recurso, poderá receber o código 2FA do seu bot. Ele também pode funcionar como uma notificação de conexão."), @@ -630,19 +630,19 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("cancel-2fa-confirm-tip", "Tem certeza de que deseja cancelar a 2FA?"), ("cancel-bot-confirm-tip", "Tem certeza de que deseja cancelar o bot do Telegram?"), ("About RustDesk", "Sobre RustDesk"), - ("Send clipboard keystrokes", ""), + ("Send clipboard keystrokes", "Colar área de transferência"), ("network_error_tip", "Por favor, verifique sua conexão de rede e clique em tentar novamente."), ("Unlock with PIN", "Desbloquear com PIN"), - ("Requires at least {} characters", ""), + ("Requires at least {} characters", "São necessários pelo menos {} caracteres"), ("Wrong PIN", "PIN Errado"), ("Set PIN", "Definir PIN"), - ("Enable trusted devices", ""), - ("Manage trusted devices", ""), - ("Platform", ""), - ("Days remaining", ""), - ("enable-trusted-devices-tip", ""), - ("Parent directory", ""), - ("Resume", ""), - ("Invalid file name", ""), + ("Enable trusted devices", "Habilitar dispositivos confiáveis"), + ("Manage trusted devices", "Gerenciar dispositivos confiáveis"), + ("Platform", "Plataforma"), + ("Days remaining", "Dias restantes"), + ("enable-trusted-devices-tip", "Ignore a verificação de dois fatores em dispositivos confiáveis"), + ("Parent directory", "Diretório pai"), + ("Resume", "Continuar"), + ("Invalid file name", "Nome de arquivo inválido"), ].iter().cloned().collect(); } From f0ca4b9fee666ec25c903e0c222894c591f09f63 Mon Sep 17 00:00:00 2001 From: 21pages Date: Fri, 6 Sep 2024 14:43:38 +0800 Subject: [PATCH 166/541] --no-server parameter to avoid server ipc occupied by ui (#9272) Signed-off-by: 21pages --- src/core_main.rs | 11 +++++++---- src/ipc.rs | 6 +++--- src/main.rs | 2 +- src/platform/linux.rs | 1 + src/server.rs | 31 +++++++++++++++++++++---------- 5 files changed, 33 insertions(+), 18 deletions(-) diff --git a/src/core_main.rs b/src/core_main.rs index 3aa69f8f367d..375b1dbc5731 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -39,6 +39,7 @@ pub fn core_main() -> Option> { let mut _is_run_as_system = false; let mut _is_quick_support = false; let mut _is_flutter_invoke_new_connection = false; + let mut no_server = false; let mut arg_exe = Default::default(); for arg in std::env::args() { if i == 0 { @@ -62,6 +63,8 @@ pub fn core_main() -> Option> { _is_run_as_system = true; } else if arg == "--quick_support" { _is_quick_support = true; + } else if arg == "--no-server" { + no_server = true; } else { args.push(arg); } @@ -134,6 +137,7 @@ pub fn core_main() -> Option> { } } hbb_common::init_log(false, &log_name); + log::info!("main start args: {:?}, env: {:?}", args, std::env::args()); // linux uni (url) go here. #[cfg(all(target_os = "linux", feature = "flutter"))] @@ -161,9 +165,8 @@ pub fn core_main() -> Option> { #[cfg(all(feature = "flutter", feature = "plugin_framework"))] #[cfg(not(any(target_os = "android", target_os = "ios")))] init_plugins(&args); - log::info!("main start args:{:?}", args); if args.is_empty() || crate::common::is_empty_uni_link(&args[0]) { - std::thread::spawn(move || crate::start_server(false)); + std::thread::spawn(move || crate::start_server(false, no_server)); } else { #[cfg(windows)] { @@ -279,11 +282,11 @@ pub fn core_main() -> Option> { crate::privacy_mode::restore_reg_connectivity(true); #[cfg(any(target_os = "linux", target_os = "windows"))] { - crate::start_server(true); + crate::start_server(true, false); } #[cfg(target_os = "macos")] { - let handler = std::thread::spawn(move || crate::start_server(true)); + let handler = std::thread::spawn(move || crate::start_server(true, false)); crate::tray::start_tray(); // prevent server exit when encountering errors from tray hbb_common::allow_err!(handler.join()); diff --git a/src/ipc.rs b/src/ipc.rs index 2b591d107110..9815fdb748bb 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -401,10 +401,10 @@ async fn handle(data: Data, stream: &mut Connection) { std::fs::remove_file(&Config::ipc_path("")).ok(); #[cfg(target_os = "linux")] { - // https://github.com/rustdesk/rustdesk/discussions/9254, slow on some machines - hbb_common::sleep((crate::platform::SERVICE_INTERVAL * 2) as f32 / 1000.0 + 1.2) + hbb_common::sleep((crate::platform::SERVICE_INTERVAL * 2) as f32 / 1000.0) .await; - crate::run_me::<&str>(vec![]).ok(); + // https://github.com/rustdesk/rustdesk/discussions/9254 + crate::run_me::<&str>(vec!["--no-server"]).ok(); } #[cfg(target_os = "macos")] { diff --git a/src/main.rs b/src/main.rs index bc41365e3fbf..44ace8a76e56 100644 --- a/src/main.rs +++ b/src/main.rs @@ -102,7 +102,7 @@ fn main() { cli::connect_test(p, key, token); } else if let Some(p) = matches.value_of("server") { log::info!("id={}", hbb_common::config::Config::get_id()); - crate::start_server(true); + crate::start_server(true, false); } common::global_clean(); } diff --git a/src/platform/linux.rs b/src/platform/linux.rs index 90e2f52ca090..4bb666fb9c2e 100644 --- a/src/platform/linux.rs +++ b/src/platform/linux.rs @@ -1378,6 +1378,7 @@ pub fn uninstall_service(show_new_window: bool, _: bool) -> bool { Config::set_option("stop-service".into(), "".into()); return true; } + // systemctl stop will kill child processes, below may not be executed. if show_new_window { run_me_with(2); } diff --git a/src/server.rs b/src/server.rs index 547886a5c989..a973ba6aef99 100644 --- a/src/server.rs +++ b/src/server.rs @@ -456,16 +456,21 @@ pub async fn start_server(_is_server: bool) { /// * `is_server` - Whether the current client is definitely the server. /// If true, the server will be started. /// Otherwise, client will check if there's already a server and start one if not. +/// * `no_server` - If `is_server` is false, whether to start a server if not found. #[cfg(not(any(target_os = "android", target_os = "ios")))] #[tokio::main] -pub async fn start_server(is_server: bool) { - #[cfg(target_os = "linux")] - { - log::info!("DISPLAY={:?}", std::env::var("DISPLAY")); - log::info!("XAUTHORITY={:?}", std::env::var("XAUTHORITY")); - } - #[cfg(windows)] - hbb_common::platform::windows::start_cpu_performance_monitor(); +pub async fn start_server(is_server: bool, no_server: bool) { + use std::sync::Once; + static ONCE: Once = Once::new(); + ONCE.call_once(|| { + #[cfg(target_os = "linux")] + { + log::info!("DISPLAY={:?}", std::env::var("DISPLAY")); + log::info!("XAUTHORITY={:?}", std::env::var("XAUTHORITY")); + } + #[cfg(windows)] + hbb_common::platform::windows::start_cpu_performance_monitor(); + }); if is_server { crate::common::set_server_running(true); @@ -516,8 +521,14 @@ pub async fn start_server(is_server: bool) { crate::ipc::client_get_hwcodec_config_thread(0); } Err(err) => { - log::info!("server not started (will try to start): {}", err); - std::thread::spawn(|| start_server(true)); + log::info!("server not started: {err:?}, no_server: {no_server}"); + if no_server { + hbb_common::sleep(1.0).await; + std::thread::spawn(|| start_server(false, true)); + } else { + log::info!("try start server"); + std::thread::spawn(|| start_server(true, false)); + } } } } From a4cd64f0d5a0a3745d416cf85c4aac945c03c0db Mon Sep 17 00:00:00 2001 From: 21pages Date: Sat, 7 Sep 2024 10:20:52 +0800 Subject: [PATCH 167/541] fix qsv memory leak by updating ffmpeg (#9266) * fix qsv memory leak by updating ffmpeg * Memory leaks occur when destroying FFmpeg QSV VRAM encoders. This issue is resolved with FFmpeg version 7. * FFmpeg requires ffnvcodec version 12.1.14.0 or higher, and an NVIDIA driver version greater than 530. For more details, https://github.com/FFmpeg/nv-codec-headers/tree/n12.1.14.0. * The code of NVIDIA VRAM encoder is not changed, still use Video Codec SDK version 11, which is unaffected by FFmpeg. Drivers newer than 470 can support this, but we may consider an update later, as the support check by sdk code may not be accurate for FFmpeg RAM encoders. * The issue is related to FFmpeg, not libmfx. FFmpeg version 7 recommends using libvpl, but vcpkg currently lacks ports for libvpl. We can add these in the future. * D3D11 Texture Rendering: The "Shared GPU Memory" in the task manager continue increasing when using D3D11 texture render, which can exceed the GPU memory limit (e.g., reaching up to 100GB). I don't know what it is and will try to find it out. * Roughly tests on Windows, Linux, macOS, and Android for quick fix. Further testing will be performed, and I will share the results in this pr. Signed-off-by: 21pages * update flutter_gpu_texture_render, fix shared gpu memory leak while rendering Signed-off-by: 21pages --------- Signed-off-by: 21pages --- Cargo.lock | 2 +- flutter/pubspec.lock | 4 +- flutter/pubspec.yaml | 2 +- ...-release-7.0-s-qsvenc-update_bitrate.patch | 95 ------------------- ...1-android-mediacodec-encode-align-64.patch | 40 -------- ...dd-query_timeout-option-for-h264-hev.patch | 40 ++++---- ...-amfenc-reconfig-when-bitrate-change.patch | 30 +++--- .../0003-amf-colorspace.patch} | 42 ++++---- res/vcpkg/ffmpeg/portfile.cmake | 49 ++++------ res/vcpkg/ffmpeg/vcpkg.json | 2 +- vcpkg.json | 2 +- 11 files changed, 82 insertions(+), 226 deletions(-) delete mode 100644 res/vcpkg/ffmpeg/5.1/0003-use-release-7.0-s-qsvenc-update_bitrate.patch delete mode 100644 res/vcpkg/ffmpeg/7.0/0001-android-mediacodec-encode-align-64.patch rename res/vcpkg/ffmpeg/{5.1 => patch}/0001-avcodec-amfenc-add-query_timeout-option-for-h264-hev.patch (62%) rename res/vcpkg/ffmpeg/{5.1 => patch}/0002-libavcodec-amfenc-reconfig-when-bitrate-change.patch (71%) rename res/vcpkg/ffmpeg/{5.1/0004-amf-colorspace.patch => patch/0003-amf-colorspace.patch} (88%) diff --git a/Cargo.lock b/Cargo.lock index a6839b9eb127..bd0e449e7542 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3045,7 +3045,7 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hwcodec" version = "0.7.0" -source = "git+https://github.com/rustdesk-org/hwcodec#b78a69c81631dd9ccfed9df68709808193082242" +source = "git+https://github.com/rustdesk-org/hwcodec#9e8b6efd8e5d904b5325597a271ebe78f5a74f3b" dependencies = [ "bindgen 0.59.2", "cc", diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index 61d57bcba4df..62f9283a0002 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -525,8 +525,8 @@ packages: dependency: "direct main" description: path: "." - ref: "38951317afe79d953ab25733667bd96e172a80d3" - resolved-ref: "38951317afe79d953ab25733667bd96e172a80d3" + ref: "2ded7f146437a761ffe6981e2f742038f85ca68d" + resolved-ref: "2ded7f146437a761ffe6981e2f742038f85ca68d" url: "https://github.com/rustdesk-org/flutter_gpu_texture_renderer" source: git version: "0.0.1" diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index db15c74cc0a2..5d49249d49c9 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -93,7 +93,7 @@ dependencies: flutter_gpu_texture_renderer: git: url: https://github.com/rustdesk-org/flutter_gpu_texture_renderer - ref: 38951317afe79d953ab25733667bd96e172a80d3 + ref: 2ded7f146437a761ffe6981e2f742038f85ca68d uuid: ^3.0.7 auto_size_text_field: ^2.2.1 flex_color_picker: ^3.3.0 diff --git a/res/vcpkg/ffmpeg/5.1/0003-use-release-7.0-s-qsvenc-update_bitrate.patch b/res/vcpkg/ffmpeg/5.1/0003-use-release-7.0-s-qsvenc-update_bitrate.patch deleted file mode 100644 index 475fb627f3e2..000000000000 --- a/res/vcpkg/ffmpeg/5.1/0003-use-release-7.0-s-qsvenc-update_bitrate.patch +++ /dev/null @@ -1,95 +0,0 @@ -From afe89a70f6bc7ebd0a6a0a31101801b88cbd60ee Mon Sep 17 00:00:00 2001 -From: 21pages -Date: Sun, 5 May 2024 12:45:23 +0800 -Subject: [PATCH] use release/7.0's update_bitrate - -Signed-off-by: 21pages ---- - libavcodec/qsvenc.c | 39 +++++++++++++++++++++++++++++++++++++++ - libavcodec/qsvenc.h | 6 ++++++ - 2 files changed, 45 insertions(+) - -diff --git a/libavcodec/qsvenc.c b/libavcodec/qsvenc.c -index 2382c2f5f7..9b34f37eb3 100644 ---- a/libavcodec/qsvenc.c -+++ b/libavcodec/qsvenc.c -@@ -714,6 +714,11 @@ static int init_video_param(AVCodecContext *avctx, QSVEncContext *q) - brc_param_multiplier = (FFMAX(FFMAX3(target_bitrate_kbps, max_bitrate_kbps, buffer_size_in_kilobytes), - initial_delay_in_kilobytes) + 0x10000) / 0x10000; - -+ q->old_rc_buffer_size = avctx->rc_buffer_size; -+ q->old_rc_initial_buffer_occupancy = avctx->rc_initial_buffer_occupancy; -+ q->old_bit_rate = avctx->bit_rate; -+ q->old_rc_max_rate = avctx->rc_max_rate; -+ - switch (q->param.mfx.RateControlMethod) { - case MFX_RATECONTROL_CBR: - case MFX_RATECONTROL_VBR: -@@ -1657,6 +1662,39 @@ static int update_qp(AVCodecContext *avctx, QSVEncContext *q, - return updated; - } - -+static int update_bitrate(AVCodecContext *avctx, QSVEncContext *q) -+{ -+ int updated = 0; -+ int target_bitrate_kbps, max_bitrate_kbps, brc_param_multiplier; -+ int buffer_size_in_kilobytes, initial_delay_in_kilobytes; -+ -+ UPDATE_PARAM(q->old_rc_buffer_size, avctx->rc_buffer_size); -+ UPDATE_PARAM(q->old_rc_initial_buffer_occupancy, avctx->rc_initial_buffer_occupancy); -+ UPDATE_PARAM(q->old_bit_rate, avctx->bit_rate); -+ UPDATE_PARAM(q->old_rc_max_rate, avctx->rc_max_rate); -+ if (!updated) -+ return 0; -+ -+ buffer_size_in_kilobytes = avctx->rc_buffer_size / 8000; -+ initial_delay_in_kilobytes = avctx->rc_initial_buffer_occupancy / 8000; -+ target_bitrate_kbps = avctx->bit_rate / 1000; -+ max_bitrate_kbps = avctx->rc_max_rate / 1000; -+ brc_param_multiplier = (FFMAX(FFMAX3(target_bitrate_kbps, max_bitrate_kbps, buffer_size_in_kilobytes), -+ initial_delay_in_kilobytes) + 0x10000) / 0x10000; -+ -+ q->param.mfx.BufferSizeInKB = buffer_size_in_kilobytes / brc_param_multiplier; -+ q->param.mfx.InitialDelayInKB = initial_delay_in_kilobytes / brc_param_multiplier; -+ q->param.mfx.TargetKbps = target_bitrate_kbps / brc_param_multiplier; -+ q->param.mfx.MaxKbps = max_bitrate_kbps / brc_param_multiplier; -+ q->param.mfx.BRCParamMultiplier = brc_param_multiplier; -+ av_log(avctx, AV_LOG_VERBOSE, -+ "Reset BufferSizeInKB: %d; InitialDelayInKB: %d; " -+ "TargetKbps: %d; MaxKbps: %d; BRCParamMultiplier: %d\n", -+ q->param.mfx.BufferSizeInKB, q->param.mfx.InitialDelayInKB, -+ q->param.mfx.TargetKbps, q->param.mfx.MaxKbps, q->param.mfx.BRCParamMultiplier); -+ return updated; -+} -+ - static int update_parameters(AVCodecContext *avctx, QSVEncContext *q, - const AVFrame *frame) - { -@@ -1666,6 +1704,7 @@ static int update_parameters(AVCodecContext *avctx, QSVEncContext *q, - return 0; - - needReset = update_qp(avctx, q, frame); -+ needReset |= update_bitrate(avctx, q); - if (!needReset) - return 0; - -diff --git a/libavcodec/qsvenc.h b/libavcodec/qsvenc.h -index b754ac4b56..5745533165 100644 ---- a/libavcodec/qsvenc.h -+++ b/libavcodec/qsvenc.h -@@ -224,6 +224,12 @@ typedef struct QSVEncContext { - int min_qp_p; - int max_qp_b; - int min_qp_b; -+ -+ // These are used for bitrate control reset -+ int old_bit_rate; -+ int old_rc_buffer_size; -+ int old_rc_initial_buffer_occupancy; -+ int old_rc_max_rate; - } QSVEncContext; - - int ff_qsv_enc_init(AVCodecContext *avctx, QSVEncContext *q); --- -2.43.0.windows.1 - diff --git a/res/vcpkg/ffmpeg/7.0/0001-android-mediacodec-encode-align-64.patch b/res/vcpkg/ffmpeg/7.0/0001-android-mediacodec-encode-align-64.patch deleted file mode 100644 index d46c54af6da1..000000000000 --- a/res/vcpkg/ffmpeg/7.0/0001-android-mediacodec-encode-align-64.patch +++ /dev/null @@ -1,40 +0,0 @@ -From be3d9d8092720bbe4239212648d2e9c4ffd7f40c Mon Sep 17 00:00:00 2001 -From: 21pages -Date: Wed, 22 May 2024 17:09:28 +0800 -Subject: [PATCH] android mediacodec encode align 64 - -Signed-off-by: 21pages ---- - libavcodec/mediacodecenc.c | 11 ++++++----- - 1 file changed, 6 insertions(+), 5 deletions(-) - -diff --git a/libavcodec/mediacodecenc.c b/libavcodec/mediacodecenc.c -index 984014f1b1..8dcd3dcd64 100644 ---- a/libavcodec/mediacodecenc.c -+++ b/libavcodec/mediacodecenc.c -@@ -200,16 +200,17 @@ static av_cold int mediacodec_init(AVCodecContext *avctx) - ff_AMediaFormat_setString(format, "mime", codec_mime); - // Workaround the alignment requirement of mediacodec. We can't do it - // silently for AV_PIX_FMT_MEDIACODEC. -+ const int align = 64; - if (avctx->pix_fmt != AV_PIX_FMT_MEDIACODEC) { -- s->width = FFALIGN(avctx->width, 16); -- s->height = FFALIGN(avctx->height, 16); -+ s->width = FFALIGN(avctx->width, align); -+ s->height = FFALIGN(avctx->height, align); - } else { - s->width = avctx->width; - s->height = avctx->height; -- if (s->width % 16 || s->height % 16) -+ if (s->width % align || s->height % align) - av_log(avctx, AV_LOG_WARNING, -- "Video size %dx%d isn't align to 16, it may have device compatibility issue\n", -- s->width, s->height); -+ "Video size %dx%d isn't align to %d, it may have device compatibility issue\n", -+ s->width, s->height, align); - } - ff_AMediaFormat_setInt32(format, "width", s->width); - ff_AMediaFormat_setInt32(format, "height", s->height); --- -2.34.1 - diff --git a/res/vcpkg/ffmpeg/5.1/0001-avcodec-amfenc-add-query_timeout-option-for-h264-hev.patch b/res/vcpkg/ffmpeg/patch/0001-avcodec-amfenc-add-query_timeout-option-for-h264-hev.patch similarity index 62% rename from res/vcpkg/ffmpeg/5.1/0001-avcodec-amfenc-add-query_timeout-option-for-h264-hev.patch rename to res/vcpkg/ffmpeg/patch/0001-avcodec-amfenc-add-query_timeout-option-for-h264-hev.patch index 245a470d39f2..5431b3edd05f 100644 --- a/res/vcpkg/ffmpeg/5.1/0001-avcodec-amfenc-add-query_timeout-option-for-h264-hev.patch +++ b/res/vcpkg/ffmpeg/patch/0001-avcodec-amfenc-add-query_timeout-option-for-h264-hev.patch @@ -1,9 +1,9 @@ -From f0b694749b38b2cfd94df4eed10e667342c234e5 Mon Sep 17 00:00:00 2001 -From: 21pages -Date: Sat, 24 Feb 2024 15:33:24 +0800 -Subject: [PATCH 1/2] avcodec/amfenc: add query_timeout option for h264/hevc +From f6988e5424e041ff6f6e241f4d8fa69a04c05e64 Mon Sep 17 00:00:00 2001 +From: 21pages +Date: Thu, 5 Sep 2024 16:26:20 +0800 +Subject: [PATCH 1/3] avcodec/amfenc: add query_timeout option for h264/hevc -Signed-off-by: 21pages +Signed-off-by: 21pages --- libavcodec/amfenc.h | 1 + libavcodec/amfenc_h264.c | 4 ++++ @@ -11,10 +11,10 @@ Signed-off-by: 21pages 3 files changed, 9 insertions(+) diff --git a/libavcodec/amfenc.h b/libavcodec/amfenc.h -index 1ab98d2f78..e92120ea39 100644 +index 2dbd378ef8..d636673a9d 100644 --- a/libavcodec/amfenc.h +++ b/libavcodec/amfenc.h -@@ -87,6 +87,7 @@ typedef struct AmfContext { +@@ -89,6 +89,7 @@ typedef struct AmfContext { int quality; int b_frame_delta_qp; int ref_b_frame_delta_qp; @@ -23,40 +23,40 @@ index 1ab98d2f78..e92120ea39 100644 // Dynamic options, can be set after Init() call diff --git a/libavcodec/amfenc_h264.c b/libavcodec/amfenc_h264.c -index efb04589f6..f55dbc80f0 100644 +index c1d5f4054e..415828f005 100644 --- a/libavcodec/amfenc_h264.c +++ b/libavcodec/amfenc_h264.c -@@ -121,6 +121,7 @@ static const AVOption options[] = { +@@ -135,6 +135,7 @@ static const AVOption options[] = { { "aud", "Inserts AU Delimiter NAL unit", OFFSET(aud) ,AV_OPT_TYPE_BOOL, { .i64 = 0 }, 0, 1, VE }, { "log_to_dbg", "Enable AMF logging to debug output", OFFSET(log_to_dbg) , AV_OPT_TYPE_BOOL, { .i64 = 0 }, 0, 1, VE }, + { "query_timeout", "Timeout for QueryOutput call in ms", OFFSET(query_timeout), AV_OPT_TYPE_INT64, { .i64 = -1 }, -1, 1000, VE }, - { NULL } - }; -@@ -155,6 +156,9 @@ static av_cold int amf_encode_init_h264(AVCodecContext *avctx) + //Pre Analysis options + { "preanalysis", "Enable preanalysis", OFFSET(preanalysis), AV_OPT_TYPE_BOOL, {.i64 = -1 }, -1, 1, VE }, +@@ -222,6 +223,9 @@ FF_ENABLE_DEPRECATION_WARNINGS AMF_ASSIGN_PROPERTY_RATE(res, ctx->encoder, AMF_VIDEO_ENCODER_FRAMERATE, framerate); + if (ctx->query_timeout >= 0) -+ AMF_ASSIGN_PROPERTY_INT64(res, ctx->encoder, AMF_VIDEO_ENCODER_QUERY_TIMEOUT, ctx->query_timeout); ++ AMF_ASSIGN_PROPERTY_INT64(res, ctx->encoder, AMF_VIDEO_ENCODER_QUERY_TIMEOUT, ctx->query_timeout); + switch (avctx->profile) { - case FF_PROFILE_H264_BASELINE: + case AV_PROFILE_H264_BASELINE: profile = AMF_VIDEO_ENCODER_PROFILE_BASELINE; diff --git a/libavcodec/amfenc_hevc.c b/libavcodec/amfenc_hevc.c -index 8ab9330730..7a40bcad31 100644 +index 33a167aa52..65259d7153 100644 --- a/libavcodec/amfenc_hevc.c +++ b/libavcodec/amfenc_hevc.c -@@ -89,6 +89,7 @@ static const AVOption options[] = { +@@ -98,6 +98,7 @@ static const AVOption options[] = { { "aud", "Inserts AU Delimiter NAL unit", OFFSET(aud) ,AV_OPT_TYPE_BOOL,{ .i64 = 0 }, 0, 1, VE }, { "log_to_dbg", "Enable AMF logging to debug output", OFFSET(log_to_dbg), AV_OPT_TYPE_BOOL,{ .i64 = 0 }, 0, 1, VE }, + { "query_timeout", "Timeout for QueryOutput call in ms", OFFSET(query_timeout), AV_OPT_TYPE_INT64, { .i64 = -1 }, -1, 1000, VE }, - { NULL } - }; -@@ -122,6 +123,9 @@ static av_cold int amf_encode_init_hevc(AVCodecContext *avctx) + //Pre Analysis options + { "preanalysis", "Enable preanalysis", OFFSET(preanalysis), AV_OPT_TYPE_BOOL, {.i64 = -1 }, -1, 1, VE }, +@@ -183,6 +184,9 @@ FF_ENABLE_DEPRECATION_WARNINGS AMF_ASSIGN_PROPERTY_RATE(res, ctx->encoder, AMF_VIDEO_ENCODER_HEVC_FRAMERATE, framerate); @@ -64,7 +64,7 @@ index 8ab9330730..7a40bcad31 100644 + AMF_ASSIGN_PROPERTY_INT64(res, ctx->encoder, AMF_VIDEO_ENCODER_HEVC_QUERY_TIMEOUT, ctx->query_timeout); + switch (avctx->profile) { - case FF_PROFILE_HEVC_MAIN: + case AV_PROFILE_HEVC_MAIN: profile = AMF_VIDEO_ENCODER_HEVC_PROFILE_MAIN; -- 2.43.0.windows.1 diff --git a/res/vcpkg/ffmpeg/5.1/0002-libavcodec-amfenc-reconfig-when-bitrate-change.patch b/res/vcpkg/ffmpeg/patch/0002-libavcodec-amfenc-reconfig-when-bitrate-change.patch similarity index 71% rename from res/vcpkg/ffmpeg/5.1/0002-libavcodec-amfenc-reconfig-when-bitrate-change.patch rename to res/vcpkg/ffmpeg/patch/0002-libavcodec-amfenc-reconfig-when-bitrate-change.patch index 13b055ef2894..62b86d08bd64 100644 --- a/res/vcpkg/ffmpeg/5.1/0002-libavcodec-amfenc-reconfig-when-bitrate-change.patch +++ b/res/vcpkg/ffmpeg/patch/0002-libavcodec-amfenc-reconfig-when-bitrate-change.patch @@ -1,16 +1,16 @@ -From 4d0d20d96ad458cfec0444b9be0182ca6085ee0c Mon Sep 17 00:00:00 2001 -From: 21pages -Date: Sat, 24 Feb 2024 16:02:44 +0800 -Subject: [PATCH 2/2] libavcodec/amfenc: reconfig when bitrate change +From 6e76c57cf2c0e790228f19c88089eef110fd74aa Mon Sep 17 00:00:00 2001 +From: 21pages +Date: Thu, 5 Sep 2024 16:32:16 +0800 +Subject: [PATCH 2/3] libavcodec/amfenc: reconfig when bitrate change -Signed-off-by: 21pages +Signed-off-by: 21pages --- libavcodec/amfenc.c | 20 ++++++++++++++++++++ libavcodec/amfenc.h | 1 + 2 files changed, 21 insertions(+) diff --git a/libavcodec/amfenc.c b/libavcodec/amfenc.c -index a033e1220e..3eab01a903 100644 +index 061859f85c..97587fe66b 100644 --- a/libavcodec/amfenc.c +++ b/libavcodec/amfenc.c @@ -222,6 +222,7 @@ static int amf_init_context(AVCodecContext *avctx) @@ -21,7 +21,7 @@ index a033e1220e..3eab01a903 100644 // configure AMF logger // the return of these functions indicates old state and do not affect behaviour -@@ -575,6 +576,23 @@ static void amf_release_buffer_with_frame_ref(AMFBuffer *frame_ref_storage_buffe +@@ -583,6 +584,23 @@ static void amf_release_buffer_with_frame_ref(AMFBuffer *frame_ref_storage_buffe frame_ref_storage_buffer->pVtbl->Release(frame_ref_storage_buffer); } @@ -45,9 +45,9 @@ index a033e1220e..3eab01a903 100644 int ff_amf_receive_packet(AVCodecContext *avctx, AVPacket *avpkt) { AmfContext *ctx = avctx->priv_data; -@@ -586,6 +604,8 @@ int ff_amf_receive_packet(AVCodecContext *avctx, AVPacket *avpkt) - AVFrame *frame = ctx->delayed_frame; - int block_and_wait; +@@ -596,6 +614,8 @@ int ff_amf_receive_packet(AVCodecContext *avctx, AVPacket *avpkt) + int query_output_data_flag = 0; + AMF_RESULT res_resubmit; + reconfig_encoder(avctx); + @@ -55,13 +55,13 @@ index a033e1220e..3eab01a903 100644 return AVERROR(EINVAL); diff --git a/libavcodec/amfenc.h b/libavcodec/amfenc.h -index e92120ea39..31172645f2 100644 +index d636673a9d..09506ee2e0 100644 --- a/libavcodec/amfenc.h +++ b/libavcodec/amfenc.h -@@ -107,6 +107,7 @@ typedef struct AmfContext { - int me_half_pel; - int me_quarter_pel; - int aud; +@@ -113,6 +113,7 @@ typedef struct AmfContext { + int max_b_frames; + int qvbr_quality_level; + int hw_high_motion_quality_boost; + int64_t av_bitrate; // HEVC - specific options diff --git a/res/vcpkg/ffmpeg/5.1/0004-amf-colorspace.patch b/res/vcpkg/ffmpeg/patch/0003-amf-colorspace.patch similarity index 88% rename from res/vcpkg/ffmpeg/5.1/0004-amf-colorspace.patch rename to res/vcpkg/ffmpeg/patch/0003-amf-colorspace.patch index 49aef6947952..9bcb6e6926c1 100644 --- a/res/vcpkg/ffmpeg/5.1/0004-amf-colorspace.patch +++ b/res/vcpkg/ffmpeg/patch/0003-amf-colorspace.patch @@ -1,32 +1,32 @@ -From 8fd62e4ecd058b09abf8847be5fbbf0eef44a90f Mon Sep 17 00:00:00 2001 +From 14b77216106eaaff9cf701528039ae4264eaf420 Mon Sep 17 00:00:00 2001 From: 21pages -Date: Tue, 16 Jul 2024 14:58:33 +0800 -Subject: [PATCH] amf colorspace +Date: Thu, 5 Sep 2024 16:41:59 +0800 +Subject: [PATCH 3/3] amf colorspace Signed-off-by: 21pages --- libavcodec/amfenc.h | 1 + - libavcodec/amfenc_h264.c | 39 +++++++++++++++++++++++++++++++++ + libavcodec/amfenc_h264.c | 40 ++++++++++++++++++++++++++++++++++ libavcodec/amfenc_hevc.c | 47 ++++++++++++++++++++++++++++++++++++++++ - 3 files changed, 87 insertions(+) + 3 files changed, 88 insertions(+) diff --git a/libavcodec/amfenc.h b/libavcodec/amfenc.h -index 31172645f2..493e01603d 100644 +index 09506ee2e0..7f458b14f7 100644 --- a/libavcodec/amfenc.h +++ b/libavcodec/amfenc.h -@@ -23,6 +23,7 @@ - +@@ -24,6 +24,7 @@ #include #include + #include +#include #include "libavutil/fifo.h" diff --git a/libavcodec/amfenc_h264.c b/libavcodec/amfenc_h264.c -index f55dbc80f0..5a6b6e164f 100644 +index 415828f005..7da5a96c71 100644 --- a/libavcodec/amfenc_h264.c +++ b/libavcodec/amfenc_h264.c -@@ -139,6 +139,9 @@ static av_cold int amf_encode_init_h264(AVCodecContext *avctx) +@@ -200,6 +200,9 @@ static av_cold int amf_encode_init_h264(AVCodecContext *avctx) AMFRate framerate; AMFSize framesize = AMFConstructSize(avctx->width, avctx->height); int deblocking_filter = (avctx->flags & AV_CODEC_FLAG_LOOP_FILTER) ? 1 : 0; @@ -36,7 +36,7 @@ index f55dbc80f0..5a6b6e164f 100644 if (avctx->framerate.num > 0 && avctx->framerate.den > 0) { framerate = AMFConstructRate(avctx->framerate.num, avctx->framerate.den); -@@ -199,11 +202,47 @@ static av_cold int amf_encode_init_h264(AVCodecContext *avctx) +@@ -266,10 +269,47 @@ FF_ENABLE_DEPRECATION_WARNINGS AMF_ASSIGN_PROPERTY_RATIO(res, ctx->encoder, AMF_VIDEO_ENCODER_ASPECT_RATIO, ratio); } @@ -70,25 +70,25 @@ index f55dbc80f0..5a6b6e164f 100644 + color_profile = AMF_VIDEO_CONVERTER_COLOR_PROFILE_2020; + break; + } -+ } + } + pix_fmt = avctx->hw_frames_ctx ? ((AVHWFramesContext*)avctx->hw_frames_ctx->data)->sw_format : avctx->pix_fmt; + color_depth = AMF_COLOR_BIT_DEPTH_8; + if (pix_fmt == AV_PIX_FMT_P010) { + color_depth = AMF_COLOR_BIT_DEPTH_10; - } - ++ } ++ + AMF_ASSIGN_PROPERTY_INT64(res, ctx->encoder, AMF_VIDEO_ENCODER_COLOR_BIT_DEPTH, color_depth); + AMF_ASSIGN_PROPERTY_INT64(res, ctx->encoder, AMF_VIDEO_ENCODER_OUTPUT_COLOR_PROFILE, color_profile); + AMF_ASSIGN_PROPERTY_INT64(res, ctx->encoder, AMF_VIDEO_ENCODER_OUTPUT_TRANSFER_CHARACTERISTIC, (amf_int64)avctx->color_trc); + AMF_ASSIGN_PROPERTY_INT64(res, ctx->encoder, AMF_VIDEO_ENCODER_OUTPUT_COLOR_PRIMARIES, (amf_int64)avctx->color_primaries); + // autodetect rate control method if (ctx->rate_control_mode == AMF_VIDEO_ENCODER_RATE_CONTROL_METHOD_UNKNOWN) { - if (ctx->qp_i != -1 || ctx->qp_p != -1 || ctx->qp_b != -1) { diff --git a/libavcodec/amfenc_hevc.c b/libavcodec/amfenc_hevc.c -index 7a40bcad31..0260f43c81 100644 +index 65259d7153..7c930d3ccc 100644 --- a/libavcodec/amfenc_hevc.c +++ b/libavcodec/amfenc_hevc.c -@@ -106,6 +106,9 @@ static av_cold int amf_encode_init_hevc(AVCodecContext *avctx) +@@ -161,6 +161,9 @@ static av_cold int amf_encode_init_hevc(AVCodecContext *avctx) AMFRate framerate; AMFSize framesize = AMFConstructSize(avctx->width, avctx->height); int deblocking_filter = (avctx->flags & AV_CODEC_FLAG_LOOP_FILTER) ? 1 : 0; @@ -98,17 +98,17 @@ index 7a40bcad31..0260f43c81 100644 if (avctx->framerate.num > 0 && avctx->framerate.den > 0) { framerate = AMFConstructRate(avctx->framerate.num, avctx->framerate.den); -@@ -130,6 +133,9 @@ static av_cold int amf_encode_init_hevc(AVCodecContext *avctx) - case FF_PROFILE_HEVC_MAIN: +@@ -191,6 +194,9 @@ FF_ENABLE_DEPRECATION_WARNINGS + case AV_PROFILE_HEVC_MAIN: profile = AMF_VIDEO_ENCODER_HEVC_PROFILE_MAIN; break; -+ case FF_PROFILE_HEVC_MAIN_10: ++ case AV_PROFILE_HEVC_MAIN_10: + profile = AMF_VIDEO_ENCODER_HEVC_PROFILE_MAIN_10; + break; default: break; } -@@ -158,6 +164,47 @@ static av_cold int amf_encode_init_hevc(AVCodecContext *avctx) +@@ -219,6 +225,47 @@ FF_ENABLE_DEPRECATION_WARNINGS AMF_ASSIGN_PROPERTY_RATIO(res, ctx->encoder, AMF_VIDEO_ENCODER_HEVC_ASPECT_RATIO, ratio); } diff --git a/res/vcpkg/ffmpeg/portfile.cmake b/res/vcpkg/ffmpeg/portfile.cmake index dc35752ff8ba..3d4c10906dfa 100644 --- a/res/vcpkg/ffmpeg/portfile.cmake +++ b/res/vcpkg/ffmpeg/portfile.cmake @@ -1,16 +1,8 @@ -if(VCPKG_TARGET_IS_WINDOWS OR VCPKG_TARGET_IS_LINUX) - set(FF_VERSION "n5.1.5") - set(FF_SHA512 "a933f18e53207ccc277b42c9a68db00f31cefec555e6d5d7c57db3409023b2c38fd93ebe2ccfcd17ba2397adb912e93f2388241ca970b7d8bd005ccfe86d5679") -else() - set(FF_VERSION "n7.0.1") - set(FF_SHA512 "1212ebcb78fdaa103b0304373d374e41bf1fe680e1fa4ce0f60624857491c26b4dda004c490c3ef32d4a0e10f42ae6b54546f9f318e2dcfbaa116117f687bc88") -endif() - vcpkg_from_github( OUT_SOURCE_PATH SOURCE_PATH REPO ffmpeg/ffmpeg - REF "${FF_VERSION}" - SHA512 "${FF_SHA512}" + REF "n${VERSION}" + SHA512 3ba02e8b979c80bf61d55f414bdac2c756578bb36498ed7486151755c6ccf8bd8ff2b8c7afa3c5d1acd862ce48314886a86a105613c05e36601984c334f8f6bf HEAD_REF master PATCHES 0002-fix-msvc-link.patch # upstreamed in future version @@ -18,25 +10,11 @@ vcpkg_from_github( 0005-fix-nasm.patch # upstreamed in future version 0012-Fix-ssl-110-detection.patch 0013-define-WINVER.patch + patch/0001-avcodec-amfenc-add-query_timeout-option-for-h264-hev.patch + patch/0002-libavcodec-amfenc-reconfig-when-bitrate-change.patch + patch/0003-amf-colorspace.patch ) -if(VCPKG_TARGET_IS_WINDOWS OR VCPKG_TARGET_IS_LINUX) - vcpkg_apply_patches( - SOURCE_PATH ${SOURCE_PATH} - PATCHES - ${CMAKE_CURRENT_LIST_DIR}/5.1/0001-avcodec-amfenc-add-query_timeout-option-for-h264-hev.patch - ${CMAKE_CURRENT_LIST_DIR}/5.1/0002-libavcodec-amfenc-reconfig-when-bitrate-change.patch - ${CMAKE_CURRENT_LIST_DIR}/5.1/0003-use-release-7.0-s-qsvenc-update_bitrate.patch - ${CMAKE_CURRENT_LIST_DIR}/5.1/0004-amf-colorspace.patch - ) -elseif(VCPKG_TARGET_IS_ANDROID) - vcpkg_apply_patches( - SOURCE_PATH ${SOURCE_PATH} - PATCHES - ${CMAKE_CURRENT_LIST_DIR}/7.0/0001-android-mediacodec-encode-align-64.patch - ) -endif() - if(SOURCE_PATH MATCHES " ") message(FATAL_ERROR "Error: ffmpeg will not build with spaces in the path. Please use a directory with no spaces") endif() @@ -130,6 +108,7 @@ elseif(VCPKG_TARGET_IS_WINDOWS) string(APPEND OPTIONS "\ --target-os=win32 \ --toolchain=msvc \ +--cc=cl \ --enable-gpl \ --enable-d3d11va \ --enable-cuda \ @@ -210,6 +189,10 @@ endif() string(APPEND VCPKG_COMBINED_C_FLAGS_DEBUG " -I \"${CURRENT_INSTALLED_DIR}/include\"") string(APPEND VCPKG_COMBINED_C_FLAGS_RELEASE " -I \"${CURRENT_INSTALLED_DIR}/include\"") +if(VCPKG_TARGET_IS_WINDOWS) + string(APPEND VCPKG_COMBINED_C_FLAGS_DEBUG " -I \"${CURRENT_INSTALLED_DIR}/include/mfx\"") + string(APPEND VCPKG_COMBINED_C_FLAGS_RELEASE " -I \"${CURRENT_INSTALLED_DIR}/include/mfx\"") +endif() # # Setup vcpkg toolchain set(prog_env "") @@ -219,8 +202,9 @@ if(VCPKG_DETECTED_CMAKE_C_COMPILER) get_filename_component(CC_filename "${VCPKG_DETECTED_CMAKE_C_COMPILER}" NAME) set(ENV{CC} "${CC_filename}") string(APPEND OPTIONS " --cc=${CC_filename}") - - # string(APPEND OPTIONS " --host_cc=${CC_filename}") ffmpeg not yet setup for cross builds? + if(VCPKG_HOST_IS_WINDOWS) + string(APPEND OPTIONS " --host_cc=${CC_filename}") + endif() list(APPEND prog_env "${CC_path}") endif() @@ -291,6 +275,13 @@ if(VCPKG_DETECTED_CMAKE_STRIP) list(APPEND prog_env "${STRIP_path}") endif() +if(VCPKG_HOST_IS_WINDOWS) + vcpkg_acquire_msys(MSYS_ROOT PACKAGES automake1.16) + set(SHELL "${MSYS_ROOT}/usr/bin/bash.exe") + list(APPEND prog_env "${MSYS_ROOT}/usr/bin" "${MSYS_ROOT}/usr/share/automake-1.16") +else() + # find_program(SHELL bash) +endif() list(REMOVE_DUPLICATES prog_env) vcpkg_add_to_path(PREPEND ${prog_env}) diff --git a/res/vcpkg/ffmpeg/vcpkg.json b/res/vcpkg/ffmpeg/vcpkg.json index 61ff2c8b549f..f7612d9281c2 100644 --- a/res/vcpkg/ffmpeg/vcpkg.json +++ b/res/vcpkg/ffmpeg/vcpkg.json @@ -1,6 +1,6 @@ { "name": "ffmpeg", - "version": "7.0.1", + "version": "7.0.2", "port-version": 0, "description": [ "a library to decode, encode, transcode, mux, demux, stream, filter and play pretty much anything that humans and machines have created.", diff --git a/vcpkg.json b/vcpkg.json index f1d7036eb5f1..81484772aea1 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -87,7 +87,7 @@ ] }, "overrides": [ - { "name": "ffnvcodec", "version": "11.1.5.2" }, + { "name": "ffnvcodec", "version": "12.1.14.0" }, { "name": "amd-amf", "version": "1.4.29" }, { "name": "mfx-dispatch", "version": "1.35.1" } ] From c8cd564e6986b1aab9ed066f678984b1e852e96b Mon Sep 17 00:00:00 2001 From: Andrzej Rudnik Date: Sat, 7 Sep 2024 11:58:07 +0200 Subject: [PATCH 168/541] Update pl.rs (#9285) --- src/lang/pl.rs | 86 +++++++++++++++++++++++++------------------------- 1 file changed, 43 insertions(+), 43 deletions(-) diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 246de02f4a78..725d67996a6b 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -349,7 +349,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable hardware codec", "Włącz akcelerację sprzętową kodeków"), ("Unlock Security Settings", "Odblokuj ustawienia zabezpieczeń"), ("Enable audio", "Włącz dźwięk"), - ("Unlock Network Settings", "Odblokuj ustawienia Sieciowe"), + ("Unlock Network Settings", "Odblokuj ustawienia sieciowe"), ("Server", "Serwer"), ("Direct IP Access", "Bezpośredni adres IP"), ("Proxy", "Proxy"), @@ -602,47 +602,47 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("no_need_privacy_mode_no_physical_displays_tip", "Brak fizycznych wyświetlaczy, tryb prywatny nie jest potrzebny."), ("Follow remote cursor", "Podążaj za zdalnym kursorem"), ("Follow remote window focus", "Podążaj za aktywnością zdalnych okien"), - ("default_proxy_tip", ""), - ("no_audio_input_device_tip", ""), - ("Incoming", ""), - ("Outgoing", ""), - ("Clear Wayland screen selection", ""), - ("clear_Wayland_screen_selection_tip", ""), - ("confirm_clear_Wayland_screen_selection_tip", ""), - ("android_new_voice_call_tip", ""), - ("texture_render_tip", ""), - ("Use texture rendering", ""), - ("Floating window", ""), - ("floating_window_tip", ""), - ("Keep screen on", ""), - ("Never", ""), - ("During controlled", ""), - ("During service is on", ""), - ("Capture screen using DirectX", ""), - ("Back", ""), - ("Apps", ""), - ("Volume up", ""), - ("Volume down", ""), - ("Power", ""), - ("Telegram bot", ""), - ("enable-bot-tip", ""), - ("enable-bot-desc", ""), - ("cancel-2fa-confirm-tip", ""), - ("cancel-bot-confirm-tip", ""), - ("About RustDesk", ""), - ("Send clipboard keystrokes", ""), - ("network_error_tip", ""), - ("Unlock with PIN", ""), - ("Requires at least {} characters", ""), - ("Wrong PIN", ""), - ("Set PIN", ""), - ("Enable trusted devices", ""), - ("Manage trusted devices", ""), - ("Platform", ""), - ("Days remaining", ""), - ("enable-trusted-devices-tip", ""), - ("Parent directory", ""), - ("Resume", ""), - ("Invalid file name", ""), + ("default_proxy_tip", "Domyślny protokół i port to Socks5 i 1080"), + ("no_audio_input_device_tip", "Nie znaleziono urządzenia audio."), + ("Incoming", "Przychodzące"), + ("Outgoing", "Wychodzące"), + ("Clear Wayland screen selection", "Wyczyść wybór ekranu Wayland"), + ("clear_Wayland_screen_selection_tip", "Po wyczyszczeniu wyboru ekranu, możesz wybrać, który ekran chcesz udostępnić."), + ("confirm_clear_Wayland_screen_selection_tip", "Na pewno wyczyścić wybór ekranu Wayland?"), + ("android_new_voice_call_tip", "Otrzymano nowe żądanie połączenia głosowego. Jeżeli je zaakceptujesz, dźwięk przełączy się na komunikację głosową."), + ("texture_render_tip", "Użyj renderowania tekstur, aby wygładzić zdjęcia. Możesz spróbować wyłączyć tę opcję, jeżeli napotkasz problemy z renderowaniem."), + ("Use texture rendering", "Użyj renderowania tekstur"), + ("Floating window", "Okno pływające"), + ("floating_window_tip", "Pozwala zachować usługę RustDesk w tle"), + ("Keep screen on", "Pozostaw ekran włączony"), + ("Never", "Nigdy"), + ("During controlled", "Podczas sterowania"), + ("During service is on", "Gdy usługa jest uruchomiona"), + ("Capture screen using DirectX", "Przechwytuj ekran używając DirectX"), + ("Back", "Wstecz"), + ("Apps", "Aplikacje"), + ("Volume up", "Głośniej"), + ("Volume down", "Ciszej"), + ("Power", "Zasilanie"), + ("Telegram bot", "Bot Telegram"), + ("enable-bot-tip", "Jeżeli włączysz tę funkcję, możesz otrzymać kod 2FA od swojego bota. Może on również działać jako powiadomienie o połączeniu."), + ("enable-bot-desc", "1. Otwórz czat z @BotFather.\n2. Wyślij polecenie \"/newbot\". Otrzymasz token do po wykonaniu tego kroku.\n3. Rozpocznij czat z nowo utworzonym botem. Wyślij wiadomość zaczynającą się od ukośnika (\"/\"),np. \"/hello\", aby go aktywować.\n"), + ("cancel-2fa-confirm-tip", "Na pewno chcesz anulować 2FA?"), + ("cancel-bot-confirm-tip", "Na pewno chcesz anulować bot Telegram?"), + ("About RustDesk", "O programie"), + ("Send clipboard keystrokes", "Wysyła naciśnięcia klawiszy ze schowka"), + ("network_error_tip", "Sprawdź swoje połączenie sieciowe, następnie kliknij Ponów."), + ("Unlock with PIN", "Odblokuj za pomocą PIN"), + ("Requires at least {} characters", "Wymaga co najmniej {} znaków"), + ("Wrong PIN", "Niewłaściwy PIN"), + ("Set PIN", "Ustaw PIN"), + ("Enable trusted devices", "Włącz zaufane urządzenia"), + ("Manage trusted devices", "Zarządzaj zaufanymi urządzeniami"), + ("Platform", "Platforma"), + ("Days remaining", "Pozostało dni"), + ("enable-trusted-devices-tip", "Omiń weryfikację 2FA dla zaufanych urządzeń"), + ("Parent directory", "Folder nadrzędny"), + ("Resume", "Wznów"), + ("Invalid file name", "Nieprawidłowa nazwa pliku"), ].iter().cloned().collect(); } From 993862c1038e002a9526c0ae9f9f4786ed9f94b2 Mon Sep 17 00:00:00 2001 From: 21pages Date: Sun, 8 Sep 2024 12:37:41 +0800 Subject: [PATCH 169/541] quit cm process if ipc connection to ipc server closed (#9292) Signed-off-by: 21pages --- src/common.rs | 6 ++++++ src/core_main.rs | 5 ++++- src/ui_cm_interface.rs | 11 +++++++++-- src/ui_interface.rs | 4 ++++ 4 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/common.rs b/src/common.rs index 1ed9d6a8fb40..2e801e66d2f8 100644 --- a/src/common.rs +++ b/src/common.rs @@ -84,6 +84,7 @@ lazy_static::lazy_static! { // Is server logic running. The server code can invoked to run by the main process if --server is not running. static ref SERVER_RUNNING: Arc> = Default::default(); static ref IS_MAIN: bool = std::env::args().nth(1).map_or(true, |arg| !arg.starts_with("--")); + static ref IS_CM: bool = std::env::args().nth(1) == Some("--cm".to_owned()) || std::env::args().nth(1) == Some("--cm-no-ui".to_owned()); } pub struct SimpleCallOnReturn { @@ -137,6 +138,11 @@ pub fn is_main() -> bool { *IS_MAIN } +#[inline] +pub fn is_cm() -> bool { + *IS_CM +} + // Is server logic running. #[inline] pub fn is_server_running() -> bool { diff --git a/src/core_main.rs b/src/core_main.rs index 375b1dbc5731..6cf3b9d02c03 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -477,7 +477,10 @@ pub fn core_main() -> Option> { } else if args[0] == "--cm-no-ui" { #[cfg(feature = "flutter")] #[cfg(not(any(target_os = "android", target_os = "ios", target_os = "windows")))] - crate::flutter::connection_manager::start_cm_no_ui(); + { + crate::ui_interface::start_option_status_sync(); + crate::flutter::connection_manager::start_cm_no_ui(); + } return None; } else { #[cfg(all(feature = "flutter", feature = "plugin_framework"))] diff --git a/src/ui_cm_interface.rs b/src/ui_cm_interface.rs index f1748112ea6b..c9b46ff1fc16 100644 --- a/src/ui_cm_interface.rs +++ b/src/ui_cm_interface.rs @@ -625,7 +625,6 @@ pub async fn start_ipc(cm: ConnectionManager) { OPTION_ENABLE_FILE_TRANSFER, &Config::get_option(OPTION_ENABLE_FILE_TRANSFER), )); - match ipc::new_listener("_cm").await { Ok(mut incoming) => { while let Some(result) = incoming.next().await { @@ -647,7 +646,7 @@ pub async fn start_ipc(cm: ConnectionManager) { log::error!("Failed to start cm ipc server: {}", err); } } - crate::platform::quit_gui(); + quit_cm(); } #[cfg(target_os = "android")] @@ -1042,3 +1041,11 @@ pub fn close_voice_call(id: i32) { allow_err!(client.tx.send(Data::CloseVoiceCall("".to_owned()))); }; } + +#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub fn quit_cm() { + // in case of std::process::exit not work + log::info!("quit cm"); + CLIENTS.write().unwrap().clear(); + crate::platform::quit_gui(); +} diff --git a/src/ui_interface.rs b/src/ui_interface.rs index aad968bb4a51..ad2db6d01dbf 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -1138,6 +1138,7 @@ async fn check_connect_status_(reconnect: bool, rx: mpsc::UnboundedReceiver { log::error!("ipc connection closed: {}", err); + if is_cm { + crate::ui_cm_interface::quit_cm(); + } break; } #[cfg(not(any(target_os = "android", target_os = "ios")))] From 1e6944b380d3674c0aed948e955b34b934304b0c Mon Sep 17 00:00:00 2001 From: rustdesk Date: Sun, 8 Sep 2024 12:54:27 +0800 Subject: [PATCH 170/541] apply --cm-no-ui to non-windows --- src/core_main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core_main.rs b/src/core_main.rs index 6cf3b9d02c03..5d137516ee4b 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -476,7 +476,7 @@ pub fn core_main() -> Option> { crate::ui_interface::start_option_status_sync(); } else if args[0] == "--cm-no-ui" { #[cfg(feature = "flutter")] - #[cfg(not(any(target_os = "android", target_os = "ios", target_os = "windows")))] + #[cfg(not(any(target_os = "android", target_os = "ios")))] { crate::ui_interface::start_option_status_sync(); crate::flutter::connection_manager::start_cm_no_ui(); From 2922ebe22aa3a0edb52a183e82efac78b0a7d43a Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Sun, 8 Sep 2024 21:13:05 +0800 Subject: [PATCH 171/541] Fix/clipboard retry if is occupied (#9293) * fix: clipboard, retry if is occupied Signed-off-by: fufesou * fix: clipboard service, hold runtime to cm ipc Signed-off-by: fufesou * update arboard Signed-off-by: fufesou * refact: log Signed-off-by: fufesou * fix: get formats, return only not Signed-off-by: fufesou --------- Signed-off-by: fufesou --- Cargo.lock | 2 +- src/clipboard.rs | 52 ++++++++++++++++++++++++++------- src/server/clipboard_service.rs | 34 ++++++++++++++++----- src/ui_cm_interface.rs | 1 + 4 files changed, 70 insertions(+), 19 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bd0e449e7542..b2bd08d436a6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -224,7 +224,7 @@ checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" [[package]] name = "arboard" version = "3.4.0" -source = "git+https://github.com/rustdesk-org/arboard#a04bdb1b368a99691822c33bf0f7ed497d6a7a35" +source = "git+https://github.com/rustdesk-org/arboard#747ab2d9b40a5c9c5102051cf3b0bb38b4845e60" dependencies = [ "clipboard-win", "core-graphics 0.23.2", diff --git a/src/clipboard.rs b/src/clipboard.rs index 4e6295db9f24..e4bd0039062f 100644 --- a/src/clipboard.rs +++ b/src/clipboard.rs @@ -1,9 +1,10 @@ use arboard::{ClipboardData, ClipboardFormat}; use clipboard_master::{ClipboardHandler, Master, Shutdown}; -use hbb_common::{log, message_proto::*, ResultType}; +use hbb_common::{bail, log, message_proto::*, ResultType}; use std::{ sync::{mpsc::Sender, Arc, Mutex}, thread::JoinHandle, + time::Duration, }; pub const CLIPBOARD_NAME: &'static str = "clipboard"; @@ -26,6 +27,9 @@ lazy_static::lazy_static! { static ref CLIPBOARD_CTX: Arc>> = Arc::new(Mutex::new(None)); } +const CLIPBOARD_GET_MAX_RETRY: usize = 3; +const CLIPBOARD_GET_RETRY_INTERVAL_DUR: Duration = Duration::from_millis(33); + const SUPPORTED_FORMATS: &[ClipboardFormat] = &[ ClipboardFormat::Text, ClipboardFormat::Html, @@ -151,14 +155,18 @@ pub fn check_clipboard( *ctx = ClipboardContext::new().ok(); } let ctx2 = ctx.as_mut()?; - let content = ctx2.get(side, force); - if let Ok(content) = content { - if !content.is_empty() { - let mut msg = Message::new(); - let clipboards = proto::create_multi_clipboards(content); - msg.set_multi_clipboards(clipboards.clone()); - *LAST_MULTI_CLIPBOARDS.lock().unwrap() = clipboards; - return Some(msg); + match ctx2.get(side, force) { + Ok(content) => { + if !content.is_empty() { + let mut msg = Message::new(); + let clipboards = proto::create_multi_clipboards(content); + msg.set_multi_clipboards(clipboards.clone()); + *LAST_MULTI_CLIPBOARDS.lock().unwrap() = clipboards; + return Some(msg); + } + } + Err(e) => { + log::error!("Failed to get clipboard content. {}", e); } } None @@ -263,9 +271,33 @@ impl ClipboardContext { Ok(ClipboardContext { inner: board }) } + fn get_formats(&mut self, formats: &[ClipboardFormat]) -> ResultType> { + for i in 0..CLIPBOARD_GET_MAX_RETRY { + match self.inner.get_formats(SUPPORTED_FORMATS) { + Ok(data) => { + return Ok(data + .into_iter() + .filter(|c| !matches!(c, arboard::ClipboardData::None)) + .collect()) + } + Err(e) => match e { + arboard::Error::ClipboardOccupied => { + log::debug!("Failed to get clipboard formats, clipboard is occupied, retrying... {}", i + 1); + std::thread::sleep(CLIPBOARD_GET_RETRY_INTERVAL_DUR); + } + _ => { + log::error!("Failed to get clipboard formats, {}", e); + return Err(e.into()); + } + }, + } + } + bail!("Failed to get clipboard formats, clipboard is occupied, {CLIPBOARD_GET_MAX_RETRY} retries failed"); + } + pub fn get(&mut self, side: ClipboardSide, force: bool) -> ResultType> { let _lock = ARBOARD_MTX.lock().unwrap(); - let data = self.inner.get_formats(SUPPORTED_FORMATS)?; + let data = self.get_formats(SUPPORTED_FORMATS)?; if data.is_empty() { return Ok(data); } diff --git a/src/server/clipboard_service.rs b/src/server/clipboard_service.rs index e70974258575..a2a3b3153819 100644 --- a/src/server/clipboard_service.rs +++ b/src/server/clipboard_service.rs @@ -11,6 +11,8 @@ use std::{ sync::mpsc::{channel, RecvTimeoutError, Sender}, time::Duration, }; +#[cfg(windows)] +use tokio::runtime::Runtime; struct Handler { sp: EmptyExtraFieldService, @@ -18,6 +20,8 @@ struct Handler { tx_cb_result: Sender, #[cfg(target_os = "windows")] stream: Option>, + #[cfg(target_os = "windows")] + rt: Option, } pub fn new() -> GenericService { @@ -34,6 +38,8 @@ fn run(sp: EmptyExtraFieldService) -> ResultType<()> { tx_cb_result, #[cfg(target_os = "windows")] stream: None, + #[cfg(target_os = "windows")] + rt: None, }; let (tx_start_res, rx_start_res) = channel(); @@ -129,29 +135,41 @@ impl Handler { // 1. the clipboard is not used frequently. // 2. the clipboard handle is sync and will not block the main thread. #[cfg(windows)] - #[tokio::main(flavor = "current_thread")] - async fn read_clipboard_from_cm_ipc(&mut self) -> ResultType> { + fn read_clipboard_from_cm_ipc(&mut self) -> ResultType> { + if self.rt.is_none() { + self.rt = Some(Runtime::new()?); + } + let Some(rt) = &self.rt else { + // unreachable! + bail!("failed to get tokio runtime"); + }; let mut is_sent = false; if let Some(stream) = &mut self.stream { // If previous stream is still alive, reuse it. // If the previous stream is dead, `is_sent` will trigger reconnect. - is_sent = stream.send(&Data::ClipboardNonFile(None)).await.is_ok(); + is_sent = match rt.block_on(stream.send(&Data::ClipboardNonFile(None))) { + Ok(_) => true, + Err(e) => { + log::debug!("Failed to send to cm: {}", e); + false + } + }; } if !is_sent { - let mut stream = crate::ipc::connect(100, "_cm").await?; - stream.send(&Data::ClipboardNonFile(None)).await?; + let mut stream = rt.block_on(crate::ipc::connect(100, "_cm"))?; + rt.block_on(stream.send(&Data::ClipboardNonFile(None)))?; self.stream = Some(stream); } if let Some(stream) = &mut self.stream { loop { - match stream.next_timeout(800).await? { + match rt.block_on(stream.next_timeout(800))? { Some(Data::ClipboardNonFile(Some((err, mut contents)))) => { if !err.is_empty() { bail!("{}", err); } else { if contents.iter().any(|c| c.next_raw) { - match timeout(1000, stream.next_raw()).await { + match rt.block_on(timeout(1000, stream.next_raw())) { Ok(Ok(mut data)) => { for c in &mut contents { if c.next_raw { @@ -168,7 +186,7 @@ impl Handler { Err(e) => { // Reconnect to avoid the next raw data remaining in the buffer. self.stream = None; - log::debug!("failed to get raw clipboard data: {}", e); + log::debug!("Failed to get raw clipboard data: {}", e); } } } diff --git a/src/ui_cm_interface.rs b/src/ui_cm_interface.rs index c9b46ff1fc16..49f91a9dab30 100644 --- a/src/ui_cm_interface.rs +++ b/src/ui_cm_interface.rs @@ -520,6 +520,7 @@ impl IpcTaskRunner { } } Err(e) => { + log::debug!("Failed to get clipboard content. {}", e); allow_err!(self.stream.send(&Data::ClipboardNonFile(Some((format!("{}", e), vec![])))).await); } } From a2792d1527f004216f2002232bc4736636a9b58a Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Sun, 8 Sep 2024 23:07:42 +0800 Subject: [PATCH 172/541] comments (#9297) * comments Signed-off-by: fufesou * comments Signed-off-by: fufesou --------- Signed-off-by: fufesou --- src/clipboard.rs | 9 +++++++++ src/server/clipboard_service.rs | 9 ++++++--- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/clipboard.rs b/src/clipboard.rs index e4bd0039062f..329b392bba72 100644 --- a/src/clipboard.rs +++ b/src/clipboard.rs @@ -272,6 +272,15 @@ impl ClipboardContext { } fn get_formats(&mut self, formats: &[ClipboardFormat]) -> ResultType> { + // If there're multiple threads or processes trying to access the clipboard at the same time, + // the previous clipboard owner will fail to access the clipboard. + // `GetLastError()` will return `ERROR_CLIPBOARD_NOT_OPEN` (OSError(1418): Thread does not have a clipboard open) at this time. + // See https://github.com/rustdesk-org/arboard/blob/747ab2d9b40a5c9c5102051cf3b0bb38b4845e60/src/platform/windows.rs#L34 + // + // This is a common case on Windows, so we retry here. + // Related issues: + // https://github.com/rustdesk/rustdesk/issues/9263 + // https://github.com/rustdesk/rustdesk/issues/9222#issuecomment-2329233175 for i in 0..CLIPBOARD_GET_MAX_RETRY { match self.inner.get_formats(SUPPORTED_FORMATS) { Ok(data) => { diff --git a/src/server/clipboard_service.rs b/src/server/clipboard_service.rs index a2a3b3153819..d6bea7520a6c 100644 --- a/src/server/clipboard_service.rs +++ b/src/server/clipboard_service.rs @@ -131,9 +131,12 @@ impl Handler { check_clipboard(&mut self.ctx, ClipboardSide::Host, false) } - // It's ok to do async operation in the clipboard service because: - // 1. the clipboard is not used frequently. - // 2. the clipboard handle is sync and will not block the main thread. + // Read clipboard data from cm using ipc. + // + // We cannot use `#[tokio::main(flavor = "current_thread")]` here, + // because the auto-managed tokio runtime (async context) will be dropped after the call. + // The next call will create a new runtime, which will cause the previous stream to be unusable. + // So we need to manage the tokio runtime manually. #[cfg(windows)] fn read_clipboard_from_cm_ipc(&mut self) -> ResultType> { if self.rt.is_none() { From 260a82ee5c40765aff41f170183a9bd7f48b654d Mon Sep 17 00:00:00 2001 From: rustdesk Date: Mon, 9 Sep 2024 17:24:03 +0800 Subject: [PATCH 173/541] upgrade pub for flutter memory leak --- flutter/macos/Podfile.lock | 8 +- flutter/pubspec.lock | 276 ++++++++++++++++++------------------- 2 files changed, 138 insertions(+), 146 deletions(-) diff --git a/flutter/macos/Podfile.lock b/flutter/macos/Podfile.lock index a9f3c7388cfb..a29674fece3e 100644 --- a/flutter/macos/Podfile.lock +++ b/flutter/macos/Podfile.lock @@ -95,17 +95,17 @@ SPEC CHECKSUMS: desktop_drop: 69eeff437544aa619c8db7f4481b3a65f7696898 desktop_multi_window: 566489c048b501134f9d7fb6a2354c60a9126486 device_info_plus: 5401765fde0b8d062a2f8eb65510fb17e77cf07f - file_selector_macos: 468fb6b81fac7c0e88d71317f3eec34c3b008ff9 + file_selector_macos: 54fdab7caa3ac3fc43c9fac4d7d8d231277f8cf2 flutter_custom_cursor: 629957115075c672287bd0fa979d863ccf6024f7 FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 package_info_plus: 02d7a575e80f194102bef286361c6c326e4c29ce - path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c + path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 screen_retriever: 59634572a57080243dd1bf715e55b6c54f241a38 sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec texture_rgba_renderer: cbed959a3c127122194a364e14b8577bd62dc8f2 uni_links_desktop: 45900fb319df48fcdea2df0756e9c2626696b026 - url_launcher_macos: d2691c7dd33ed713bf3544850a623080ec693d95 - video_player_avfoundation: 02011213dab73ae3687df27ce441fbbcc82b5579 + url_launcher_macos: 5f437abeda8c85500ceb03f5c1938a8c5a705399 + video_player_avfoundation: 7c6c11d8470e1675df7397027218274b6d2360b3 wakelock_plus: 4783562c9a43d209c458cb9b30692134af456269 window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8 window_size: 339dafa0b27a95a62a843042038fa6c3c48de195 diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index 62f9283a0002..0a1f3ada79d0 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -5,10 +5,10 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: eb376e9acf6938204f90eb3b1f00b578640d3188b4c8a8ec054f9f479af8d051 + sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7" url: "https://pub.dev" source: hosted - version: "64.0.0" + version: "67.0.0" after_layout: dependency: transitive description: @@ -21,10 +21,10 @@ packages: dependency: transitive description: name: analyzer - sha256: "69f54f967773f6c26c7dcb13e93d7ccee8b17a641689da39e878d5cf13b06893" + sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d" url: "https://pub.dev" source: hosted - version: "6.2.0" + version: "6.4.1" animations: dependency: transitive description: @@ -37,18 +37,18 @@ packages: dependency: transitive description: name: archive - sha256: "22600aa1e926be775fa5fe7e6894e7fb3df9efda8891c73f70fb3262399a432d" + sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d url: "https://pub.dev" source: hosted - version: "3.4.10" + version: "3.6.1" args: dependency: transitive description: name: args - sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596 + sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a" url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.5.0" async: dependency: transitive description: @@ -69,10 +69,10 @@ packages: dependency: "direct main" description: name: auto_size_text_field - sha256: d47c81ffa9b61d219f6c50492dc03ea28fa9346561b2ec33b46ccdc000ddb0aa + sha256: "41c90b2270e38edc6ce5c02e5a17737a863e65e246bdfc94565a38f3ec399144" url: "https://pub.dev" source: hosted - version: "2.2.2" + version: "2.2.4" back_button_interceptor: dependency: "direct main" description: @@ -141,10 +141,10 @@ packages: dependency: "direct dev" description: name: build_runner - sha256: "581bacf68f89ec8792f5e5a0b2c4decd1c948e97ce659dc783688c8a88fbec21" + sha256: "3ac61a79bfb6f6cc11f693591063a7f19a7af628dc52f141743edac5c16e8c22" url: "https://pub.dev" source: hosted - version: "2.4.8" + version: "2.4.9" build_runner_core: dependency: transitive description: @@ -165,10 +165,10 @@ packages: dependency: transitive description: name: built_value - sha256: a3ec2e0f967bc47f69f95009bb93db936288d61d5343b9436e378b28a2f830c6 + sha256: c7913a9737ee4007efedaffc968c049fd0f3d0e49109e778edc10de9426005cb url: "https://pub.dev" source: hosted - version: "8.9.0" + version: "8.9.2" cached_network_image: dependency: transitive description: @@ -189,10 +189,10 @@ packages: dependency: transitive description: name: cached_network_image_web - sha256: "42a835caa27c220d1294311ac409a43361088625a4f23c820b006dd9bffb3316" + sha256: "205d6a9f1862de34b93184f22b9d2d94586b2f05c581d546695e3d8f6a805cd7" url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.2.0" characters: dependency: transitive description: @@ -269,10 +269,10 @@ packages: dependency: transitive description: name: cross_file - sha256: fedaadfa3a6996f75211d835aaeb8fede285dae94262485698afd832371b9a5e + sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670" url: "https://pub.dev" source: hosted - version: "0.3.3+8" + version: "0.3.4+2" crypto: dependency: transitive description: @@ -293,10 +293,10 @@ packages: dependency: transitive description: name: dart_style - sha256: "40ae61a5d43feea6d24bd22c0537a6629db858963b99b4bc1c3db80676f32368" + sha256: "99e066ce75c89d6b29903d788a7bb9369cf754f7b24bf70bf4b6d6d6b26853b9" url: "https://pub.dev" source: hosted - version: "2.3.4" + version: "2.3.6" dash_chat_2: dependency: "direct main" description: @@ -351,10 +351,10 @@ packages: dependency: transitive description: name: device_info_plus_platform_interface - sha256: d3b01d5868b50ae571cd1dc6e502fc94d956b665756180f7b16ead09e836fd64 + sha256: "282d3cf731045a2feb66abfe61bbc40870ae50a3ed10a4d3d217556c35c8c2ba" url: "https://pub.dev" source: hosted - version: "7.0.0" + version: "7.0.1" draggable_float_widget: dependency: "direct main" description: @@ -408,10 +408,10 @@ packages: dependency: "direct main" description: name: ffi - sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878" + sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.3" ffigen: dependency: "direct dev" description: @@ -448,10 +448,10 @@ packages: dependency: transitive description: name: file_selector_macos - sha256: b15c3da8bd4908b9918111fa486903f5808e388b8d1c559949f584725a6594d6 + sha256: f42eacb83b318e183b1ae24eead1373ab1334084404c8c16e0354f9a3e55d385 url: "https://pub.dev" source: hosted - version: "0.9.3+3" + version: "0.9.4" file_selector_platform_interface: dependency: transitive description: @@ -464,10 +464,10 @@ packages: dependency: transitive description: name: file_selector_windows - sha256: d3547240c20cabf205c7c7f01a50ecdbc413755814d6677f3cb366f04abcead0 + sha256: "2ad726953f6e8affbc4df8dc78b77c3b4a060967a291e528ef72ae846c60fb69" url: "https://pub.dev" source: hosted - version: "0.9.3+1" + version: "0.9.3+2" fixnum: dependency: transitive description: @@ -480,18 +480,18 @@ packages: dependency: "direct main" description: name: flex_color_picker - sha256: "0871edc170153cfc3de316d30625f40a85daecfa76ce541641f3cc0ec7757cbf" + sha256: "5c846437069fb7afdd7ade6bf37e628a71d2ab0787095ddcb1253bf9345d5f3a" url: "https://pub.dev" source: hosted - version: "3.3.1" + version: "3.4.1" flex_seed_scheme: dependency: transitive description: name: flex_seed_scheme - sha256: "29c12aba221eb8a368a119685371381f8035011d18de5ba277ad11d7dfb8657f" + sha256: "4cee2f1d07259f77e8b36f4ec5f35499d19e74e17c7dce5b819554914082bc01" url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.5.0" flutter: dependency: "direct main" description: flutter @@ -620,10 +620,10 @@ packages: dependency: transitive description: name: flutter_plugin_android_lifecycle - sha256: b068ffc46f82a55844acfa4fdbb61fad72fa2aef0905548419d97f0f95c456da + sha256: "8cf40eebf5dec866a6d1956ad7b4f7016e6c0cc69847ab946833b7d43743809f" url: "https://pub.dev" source: hosted - version: "2.0.17" + version: "2.0.19" flutter_rust_bridge: dependency: "direct main" description: @@ -636,10 +636,10 @@ packages: dependency: "direct main" description: name: flutter_svg - sha256: d39e7f95621fc84376bc0f7d504f05c3a41488c562f4a8ad410569127507402c + sha256: "7b4ca6cf3304575fe9c8ec64813c8d02ee41d2afe60bcfe0678bcb5375d596a2" url: "https://pub.dev" source: hosted - version: "2.0.9" + version: "2.0.10+1" flutter_web_plugins: dependency: transitive description: flutter @@ -649,26 +649,26 @@ packages: dependency: "direct dev" description: name: freezed - sha256: "57247f692f35f068cae297549a46a9a097100685c6780fe67177503eea5ed4e5" + sha256: a434911f643466d78462625df76fd9eb13e57348ff43fe1f77bbe909522c67a1 url: "https://pub.dev" source: hosted - version: "2.4.7" + version: "2.5.2" freezed_annotation: dependency: "direct main" description: name: freezed_annotation - sha256: c3fd9336eb55a38cc1bbd79ab17573113a8deccd0ecbbf926cca3c62803b5c2d + sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2 url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.4.4" frontend_server_client: dependency: transitive description: name: frontend_server_client - sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612" + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 url: "https://pub.dev" source: hosted - version: "3.2.0" + version: "4.0.0" get: dependency: "direct main" description: @@ -705,10 +705,10 @@ packages: dependency: "direct main" description: name: http - sha256: a2bbf9d017fcced29139daa8ed2bba4ece450ab222871df93ca9eec6f80c34ba + sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010 url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.2.2" http_multi_server: dependency: transitive description: @@ -737,10 +737,10 @@ packages: dependency: "direct main" description: name: image - sha256: "4c68bfd5ae83e700b5204c1e74451e7bf3cf750e6843c6e158289cf56bda018e" + sha256: "2237616a36c0d69aef7549ab439b833fb7f9fb9fc861af2cc9ac3eedddd69ca8" url: "https://pub.dev" source: hosted - version: "4.1.7" + version: "4.2.0" image_picker: dependency: "direct main" description: @@ -753,10 +753,10 @@ packages: dependency: transitive description: name: image_picker_android - sha256: "39f2bfe497e495450c81abcd44b62f56c2a36a37a175da7d137b4454977b51b1" + sha256: "0f57fee1e8bfadf8cc41818bbcd7f72e53bb768a54d9496355d5e8a5681a19f1" url: "https://pub.dev" source: hosted - version: "0.8.9+3" + version: "0.8.12+1" image_picker_for_web: dependency: transitive description: @@ -769,10 +769,10 @@ packages: dependency: transitive description: name: image_picker_ios - sha256: fadafce49e8569257a0cad56d24438a6fa1f0cbd7ee0af9b631f7492818a4ca3 + sha256: "6703696ad49f5c3c8356d576d7ace84d1faf459afb07accbb0fae780753ff447" url: "https://pub.dev" source: hosted - version: "0.8.9+1" + version: "0.8.12" image_picker_linux: dependency: transitive description: @@ -793,10 +793,10 @@ packages: dependency: transitive description: name: image_picker_platform_interface - sha256: fa4e815e6fcada50e35718727d83ba1c92f1edf95c0b4436554cec301b56233b + sha256: "9ec26d410ff46f483c5519c29c02ef0e02e13a543f882b152d4bfd2f06802f80" url: "https://pub.dev" source: hosted - version: "2.9.3" + version: "2.10.0" image_picker_windows: dependency: transitive description: @@ -833,10 +833,10 @@ packages: dependency: transitive description: name: json_annotation - sha256: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467 + sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" url: "https://pub.dev" source: hosted - version: "4.8.1" + version: "4.9.0" lints: dependency: transitive description: @@ -881,10 +881,10 @@ packages: dependency: transitive description: name: mime - sha256: "2e123074287cc9fd6c09de8336dae606d1ddb88d9ac47358826db698c176a1f2" + sha256: "801fd0b26f14a4a58ccb09d5892c3fbdeff209594300a542492cf13fba9d247a" url: "https://pub.dev" source: hosted - version: "1.0.5" + version: "1.0.6" nested: dependency: transitive description: @@ -897,10 +897,10 @@ packages: dependency: transitive description: name: octo_image - sha256: "45b40f99622f11901238e18d48f5f12ea36426d8eced9f4cbf58479c7aa2430d" + sha256: "34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd" url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.1.0" package_config: dependency: transitive description: @@ -953,26 +953,26 @@ packages: dependency: "direct main" description: name: path_provider - sha256: b27217933eeeba8ff24845c34003b003b2b22151de3c908d0e679e8fe1aa078b + sha256: fec0d61223fba3154d87759e3cc27fe2c8dc498f6386c6d6fc80d1afdd1bf378 url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.4" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: "477184d672607c0a3bf68fbbf601805f92ef79c82b64b4d6eb318cbca4c48668" + sha256: a248d8146ee5983446bf03ed5ea8f6533129a12b11f12057ad1b4a67a2b3b41d url: "https://pub.dev" source: hosted - version: "2.2.2" + version: "2.2.4" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: "5a7999be66e000916500be4f15a3633ebceb8302719b47b9cc49ce924125350f" + sha256: f234384a3fdd67f989b4d54a5d73ca2a6c422fa55ae694381ae0f4375cd1ea16 url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.4.0" path_provider_linux: dependency: transitive description: @@ -993,10 +993,10 @@ packages: dependency: transitive description: name: path_provider_windows - sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.3.0" pedantic: dependency: transitive description: @@ -1025,10 +1025,10 @@ packages: dependency: transitive description: name: platform - sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" + sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" url: "https://pub.dev" source: hosted - version: "3.1.4" + version: "3.1.5" plugin_platform_interface: dependency: transitive description: @@ -1037,14 +1037,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.8" - pointycastle: - dependency: transitive - description: - name: pointycastle - sha256: "43ac87de6e10afabc85c445745a7b799e04de84cebaa4fd7bf55a5e1e9604d29" - url: "https://pub.dev" - source: hosted - version: "3.7.4" pool: dependency: transitive description: @@ -1057,10 +1049,10 @@ packages: dependency: "direct main" description: name: provider - sha256: "9a96a0a19b594dbc5bf0f1f27d2bc67d5f95957359b461cd9feb44ed6ae75096" + sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c url: "https://pub.dev" source: hosted - version: "6.1.1" + version: "6.1.2" pub_semver: dependency: transitive description: @@ -1073,26 +1065,26 @@ packages: dependency: transitive description: name: pubspec_parse - sha256: c63b2876e58e194e4b0828fcb080ad0e06d051cb607a6be51a9e084f47cb9367 + sha256: c799b721d79eb6ee6fa56f00c04b472dcd44a30d258fac2174a6ec57302678f8 url: "https://pub.dev" source: hosted - version: "1.2.3" + version: "1.3.0" pull_down_button: dependency: "direct main" description: name: pull_down_button - sha256: "235b302701ce029fd9e9470975069376a6700935bb47a5f1b3ec8a5efba07e6f" + sha256: "48b928203afdeafa4a8be5dc96980523bc8a2ddbd04569f766071a722be22379" url: "https://pub.dev" source: hosted - version: "0.9.3" + version: "0.9.4" puppeteer: dependency: transitive description: name: puppeteer - sha256: eedeaae6ec5d2e54f9ae22ab4d6b3dda2e8791c356cc783046d06c287ffe11d8 + sha256: a6752d4f09b510ae41911bfd0997f957e723d38facf320dd9ee0e5661108744a url: "https://pub.dev" source: hosted - version: "3.6.0" + version: "3.13.0" qr: dependency: transitive description: @@ -1121,10 +1113,10 @@ packages: dependency: transitive description: name: quiver - sha256: b1c1ac5ce6688d77f65f3375a9abb9319b3cb32486bdc7a1e0fdf004d7ba4e47 + sha256: ea0b925899e64ecdfbf9c7becb60d5b50e706ade44a85b2363be2a22d88117d2 url: "https://pub.dev" source: hosted - version: "3.2.1" + version: "3.2.2" rxdart: dependency: transitive description: @@ -1169,10 +1161,10 @@ packages: dependency: transitive description: name: shelf_static - sha256: a41d3f53c4adf0f57480578c1d61d90342cd617de7fc8077b1304643c2d85c1e + sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3 url: "https://pub.dev" source: hosted - version: "1.1.2" + version: "1.1.3" shelf_web_socket: dependency: transitive description: @@ -1214,18 +1206,18 @@ packages: dependency: transitive description: name: sqflite - sha256: a9016f495c927cb90557c909ff26a6d92d9bd54fc42ba92e19d4e79d61e798c6 + sha256: a43e5a27235518c03ca238e7b4732cf35eabe863a369ceba6cbefa537a66f16d url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.3.3+1" sqflite_common: dependency: transitive description: name: sqflite_common - sha256: "28d8c66baee4968519fb8bd6cdbedad982d6e53359091f0b74544a9f32ec72d5" + sha256: "3da423ce7baf868be70e2c0976c28a1bb2f73644268b7ffa7d2e08eab71f16a4" url: "https://pub.dev" source: hosted - version: "2.5.3" + version: "2.5.4" stack_trace: dependency: transitive description: @@ -1254,10 +1246,10 @@ packages: dependency: transitive description: name: string_scanner - sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.0" synchronized: dependency: transitive description: @@ -1302,10 +1294,10 @@ packages: dependency: "direct main" description: name: toggle_switch - sha256: "9e6af1f0c5a97d9de41109dc7b9e1b3bbe73417f89b10e0e44dc834fb493d4cb" + sha256: dca04512d7c23ed320d6c5ede1211a404f177d54d353bf785b07d15546a86ce5 url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.3.0" tuple: dependency: "direct main" description: @@ -1366,66 +1358,66 @@ packages: dependency: "direct main" description: name: url_launcher - sha256: c512655380d241a337521703af62d2c122bf7b77a46ff7dd750092aa9433499c + sha256: "21b704ce5fa560ea9f3b525b43601c678728ba46725bab9b01187b4831377ed3" url: "https://pub.dev" source: hosted - version: "6.2.4" + version: "6.3.0" url_launcher_android: dependency: transitive description: name: url_launcher_android - sha256: "507dc655b1d9cb5ebc756032eb785f114e415f91557b73bf60b7e201dfedeb2f" + sha256: "17cd5e205ea615e2c6ea7a77323a11712dffa0720a8a90540db57a01347f9ad9" url: "https://pub.dev" source: hosted - version: "6.2.2" + version: "6.3.2" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: "75bb6fe3f60070407704282a2d295630cab232991eb52542b18347a8a941df03" + sha256: e43b677296fadce447e987a2f519dcf5f6d1e527dc35d01ffab4fff5b8a7063e url: "https://pub.dev" source: hosted - version: "6.2.4" + version: "6.3.1" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - sha256: ab360eb661f8879369acac07b6bb3ff09d9471155357da8443fd5d3cf7363811 + sha256: e2b9622b4007f97f504cd64c0128309dfb978ae66adbe944125ed9e1750f06af url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.2.0" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - sha256: b7244901ea3cf489c5335bdacda07264a6e960b1c1b1a9f91e4bc371d9e68234 + sha256: "9a1a42d5d2d95400c795b2914c36fdcb525870c752569438e4ebb09a2b5d90de" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.2.0" url_launcher_platform_interface: dependency: transitive description: name: url_launcher_platform_interface - sha256: a932c3a8082e118f80a475ce692fde89dc20fddb24c57360b96bc56f7035de1f + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.3.2" url_launcher_web: dependency: transitive description: name: url_launcher_web - sha256: fff0932192afeedf63cdd50ecbb1bc825d31aed259f02bb8dba0f3b729a5e88b + sha256: "772638d3b34c779ede05ba3d38af34657a05ac55b06279ea6edd409e323dca8e" url: "https://pub.dev" source: hosted - version: "2.2.3" + version: "2.3.3" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - sha256: ecf9725510600aa2bb6d7ddabe16357691b6d2805f66216a97d1b881e21beff7 + sha256: "49c10f879746271804767cb45551ec5592cdab00ee105c06dddde1a98f73b185" url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.1.2" uuid: dependency: "direct main" description: @@ -1438,26 +1430,26 @@ packages: dependency: transitive description: name: vector_graphics - sha256: "4ac59808bbfca6da38c99f415ff2d3a5d7ca0a6b4809c71d9cf30fba5daf9752" + sha256: "32c3c684e02f9bc0afb0ae0aa653337a2fe022e8ab064bcd7ffda27a74e288e3" url: "https://pub.dev" source: hosted - version: "1.1.10+1" + version: "1.1.11+1" vector_graphics_codec: dependency: transitive description: name: vector_graphics_codec - sha256: f3247e7ab0ec77dc759263e68394990edc608fb2b480b80db8aa86ed09279e33 + sha256: c86987475f162fadff579e7320c7ddda04cd2fdeffbe1129227a85d9ac9e03da url: "https://pub.dev" source: hosted - version: "1.1.10+1" + version: "1.1.11+1" vector_graphics_compiler: dependency: transitive description: name: vector_graphics_compiler - sha256: "18489bdd8850de3dd7ca8a34e0c446f719ec63e2bab2e7a8cc66a9028dd76c5a" + sha256: "12faff3f73b1741a36ca7e31b292ddeb629af819ca9efe9953b70bd63fc8cd81" url: "https://pub.dev" source: hosted - version: "1.1.10+1" + version: "1.1.11+1" vector_math: dependency: transitive description: @@ -1470,26 +1462,26 @@ packages: dependency: transitive description: name: video_player - sha256: fbf28ce8bcfe709ad91b5789166c832cb7a684d14f571a81891858fefb5bb1c2 + sha256: e30df0d226c4ef82e2c150ebf6834b3522cf3f654d8e2f9419d376cdc071425d url: "https://pub.dev" source: hosted - version: "2.8.2" + version: "2.9.1" video_player_android: dependency: transitive description: name: video_player_android - sha256: "7f8f25d7ad56819a82b2948357f3c3af071f6a678db33833b26ec36bbc221316" + sha256: "134e1ad410d67e18a19486ed9512c72dfc6d8ffb284d0e8f2e99e903d1ba8fa3" url: "https://pub.dev" source: hosted - version: "2.4.11" + version: "2.4.14" video_player_avfoundation: dependency: transitive description: name: video_player_avfoundation - sha256: "309e3962795e761be010869bae65c0b0e45b5230c5cee1bec72197ca7db040ed" + sha256: d1e9a824f2b324000dc8fb2dcb2a3285b6c1c7c487521c63306cc5b394f68a7c url: "https://pub.dev" source: hosted - version: "2.5.6" + version: "2.6.1" video_player_platform_interface: dependency: transitive description: @@ -1502,10 +1494,10 @@ packages: dependency: transitive description: name: video_player_web - sha256: "34beb3a07d4331a24f7e7b2f75b8e2b103289038e07e65529699a671b6a6e2cb" + sha256: "6dcdd298136523eaf7dfc31abaf0dfba9aa8a8dbc96670e87e9d42b6f2caf774" url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.3.2" visibility_detector: dependency: "direct main" description: @@ -1518,18 +1510,18 @@ packages: dependency: "direct main" description: name: wakelock_plus - sha256: f268ca2116db22e57577fb99d52515a24bdc1d570f12ac18bb762361d43b043d + sha256: "104d94837bb28c735894dcd592877e990149c380e6358b00c04398ca1426eed4" url: "https://pub.dev" source: hosted - version: "1.1.4" + version: "1.2.1" wakelock_plus_platform_interface: dependency: transitive description: name: wakelock_plus_platform_interface - sha256: "40fabed5da06caff0796dc638e1f07ee395fb18801fbff3255a2372db2d80385" + sha256: "422d1cdbb448079a8a62a5a770b69baa489f8f7ca21aef47800c726d404f9d16" url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.2.1" watcher: dependency: transitive description: @@ -1542,34 +1534,34 @@ packages: dependency: transitive description: name: web - sha256: "4188706108906f002b3a293509234588823c8c979dc83304e229ff400c996b05" + sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27" url: "https://pub.dev" source: hosted - version: "0.4.2" + version: "0.5.1" web_socket_channel: dependency: transitive description: name: web_socket_channel - sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b + sha256: "58c6666b342a38816b2e7e50ed0f1e261959630becd4c879c4f26bfa14aa5a42" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.4.5" win32: dependency: "direct main" description: name: win32 - sha256: "464f5674532865248444b4c3daca12bd9bf2d7c47f759ce2617986e7229494a8" + sha256: "0eaf06e3446824099858367950a813472af675116bf63f008a4c2a75ae13e9cb" url: "https://pub.dev" source: hosted - version: "5.2.0" + version: "5.5.0" win32_registry: dependency: transitive description: name: win32_registry - sha256: "41fd8a189940d8696b1b810efb9abcf60827b6cbfab90b0c43e8439e3a39d85a" + sha256: "10589e0d7f4e053f2c61023a31c9ce01146656a70b7b7f0828c0b46d7da2a9bb" url: "https://pub.dev" source: hosted - version: "1.1.2" + version: "1.1.3" window_manager: dependency: "direct main" description: @@ -1616,18 +1608,18 @@ packages: dependency: transitive description: name: yaml_edit - sha256: "1579d4a0340a83cf9e4d580ea51a16329c916973bffd5bd4b45e911b25d46bfd" + sha256: e9c1a3543d2da0db3e90270dbb1e4eebc985ee5e3ffe468d83224472b2194a5f url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.2.1" zxing2: dependency: "direct main" description: name: zxing2 - sha256: a042961441bd400f59595f9125ef5fca4c888daf0ea59c17f41e0e151f8a12b5 + sha256: "6cf995abd3c86f01ba882968dedffa7bc130185e382f2300239d2e857fc7912c" url: "https://pub.dev" source: hosted - version: "0.2.1" + version: "0.2.3" sdks: dart: ">=3.3.0 <4.0.0" flutter: ">=3.19.0" From 943f96ef8c937b07878ed019aea032f0debea9a5 Mon Sep 17 00:00:00 2001 From: 21pages Date: Tue, 10 Sep 2024 10:57:01 +0800 Subject: [PATCH 174/541] downgrade url_launcher/uni_links for linux ci (#9306) Signed-off-by: 21pages --- flutter/pubspec.lock | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index 0a1f3ada79d0..e48e74a53d3a 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -1358,50 +1358,50 @@ packages: dependency: "direct main" description: name: url_launcher - sha256: "21b704ce5fa560ea9f3b525b43601c678728ba46725bab9b01187b4831377ed3" + sha256: c512655380d241a337521703af62d2c122bf7b77a46ff7dd750092aa9433499c url: "https://pub.dev" source: hosted - version: "6.3.0" + version: "6.2.4" url_launcher_android: dependency: transitive description: name: url_launcher_android - sha256: "17cd5e205ea615e2c6ea7a77323a11712dffa0720a8a90540db57a01347f9ad9" + sha256: "507dc655b1d9cb5ebc756032eb785f114e415f91557b73bf60b7e201dfedeb2f" url: "https://pub.dev" source: hosted - version: "6.3.2" + version: "6.2.2" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: e43b677296fadce447e987a2f519dcf5f6d1e527dc35d01ffab4fff5b8a7063e + sha256: "75bb6fe3f60070407704282a2d295630cab232991eb52542b18347a8a941df03" url: "https://pub.dev" source: hosted - version: "6.3.1" + version: "6.2.4" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - sha256: e2b9622b4007f97f504cd64c0128309dfb978ae66adbe944125ed9e1750f06af + sha256: ab360eb661f8879369acac07b6bb3ff09d9471155357da8443fd5d3cf7363811 url: "https://pub.dev" source: hosted - version: "3.2.0" + version: "3.1.1" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - sha256: "9a1a42d5d2d95400c795b2914c36fdcb525870c752569438e4ebb09a2b5d90de" + sha256: b7244901ea3cf489c5335bdacda07264a6e960b1c1b1a9f91e4bc371d9e68234 url: "https://pub.dev" source: hosted - version: "3.2.0" + version: "3.1.0" url_launcher_platform_interface: dependency: transitive description: name: url_launcher_platform_interface - sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" + sha256: a932c3a8082e118f80a475ce692fde89dc20fddb24c57360b96bc56f7035de1f url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.3.1" url_launcher_web: dependency: transitive description: @@ -1414,10 +1414,10 @@ packages: dependency: transitive description: name: url_launcher_windows - sha256: "49c10f879746271804767cb45551ec5592cdab00ee105c06dddde1a98f73b185" + sha256: ecf9725510600aa2bb6d7ddabe16357691b6d2805f66216a97d1b881e21beff7 url: "https://pub.dev" source: hosted - version: "3.1.2" + version: "3.1.1" uuid: dependency: "direct main" description: From 13effe7f141f367e3988c02291b3970580764f5c Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Tue, 10 Sep 2024 11:29:20 +0800 Subject: [PATCH 175/541] fix: web, switch display (#9307) Signed-off-by: fufesou --- flutter/lib/models/model.dart | 11 +++++++---- flutter/lib/web/bridge.dart | 6 +++--- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index c622d9f10d88..853cf8ac073e 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -497,10 +497,12 @@ class FfiModel with ChangeNotifier { newDisplay.width = int.tryParse(evt['width']) ?? newDisplay.width; newDisplay.height = int.tryParse(evt['height']) ?? newDisplay.height; newDisplay.cursorEmbedded = int.tryParse(evt['cursor_embedded']) == 1; - newDisplay.originalWidth = - int.tryParse(evt['original_width']) ?? kInvalidResolutionValue; - newDisplay.originalHeight = - int.tryParse(evt['original_height']) ?? kInvalidResolutionValue; + newDisplay.originalWidth = int.tryParse( + evt['original_width'] ?? kInvalidResolutionValue.toString()) ?? + kInvalidResolutionValue; + newDisplay.originalHeight = int.tryParse( + evt['original_height'] ?? kInvalidResolutionValue.toString()) ?? + kInvalidResolutionValue; newDisplay._scale = _pi.scaleOfDisplay(display); _pi.displays[display] = newDisplay; @@ -2509,6 +2511,7 @@ class FFI { onEvent2UIRgba(); imageModel.onRgba(display, data); }); + this.id = id; return; } diff --git a/flutter/lib/web/bridge.dart b/flutter/lib/web/bridge.dart index d1b777dd1e4d..97cac31c9347 100644 --- a/flutter/lib/web/bridge.dart +++ b/flutter/lib/web/bridge.dart @@ -391,9 +391,9 @@ class RustdeskImpl { return Future(() => js.context.callMethod('setByName', [ 'switch_display', jsonEncode({ - isDesktop: isDesktop, - sessionId: sessionId.toString(), - value: value + 'isDesktop': isDesktop, + 'sessionId': sessionId.toString(), + 'value': value }) ])); } From 51055a7e5b6a8587ea255a386c1ebff1a69aa304 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Tue, 10 Sep 2024 17:39:22 +0800 Subject: [PATCH 176/541] fix: tokio, call future in context of runtime (#9310) Signed-off-by: fufesou --- src/server/clipboard_service.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/server/clipboard_service.rs b/src/server/clipboard_service.rs index d6bea7520a6c..55ebbc2f2ec5 100644 --- a/src/server/clipboard_service.rs +++ b/src/server/clipboard_service.rs @@ -133,7 +133,7 @@ impl Handler { // Read clipboard data from cm using ipc. // - // We cannot use `#[tokio::main(flavor = "current_thread")]` here, + // We cannot use `#[tokio::main(flavor = "current_thread")]` here, // because the auto-managed tokio runtime (async context) will be dropped after the call. // The next call will create a new runtime, which will cause the previous stream to be unusable. // So we need to manage the tokio runtime manually. @@ -172,7 +172,13 @@ impl Handler { bail!("{}", err); } else { if contents.iter().any(|c| c.next_raw) { - match rt.block_on(timeout(1000, stream.next_raw())) { + // Wrap the future with a `Timeout` in an async block to avoid panic. + // We cannot use `rt.block_on(timeout(1000, stream.next_raw()))` here, because it causes panic: + // thread '' panicked at D:\Projects\rust\rustdesk\libs\hbb_common\src\lib.rs:98:5: + // there is no reactor running, must be called from the context of a Tokio 1.x runtime + // note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + match rt.block_on(async { timeout(1000, stream.next_raw()).await }) + { Ok(Ok(mut data)) => { for c in &mut contents { if c.next_raw { From 1f2a75fbd89fa362e0adc333000446ce8731929d Mon Sep 17 00:00:00 2001 From: rustdesk Date: Tue, 10 Sep 2024 21:28:07 +0800 Subject: [PATCH 177/541] revert back pub lock because it does not fix leak --- flutter/pubspec.lock | 248 ++++++++++++++++++++++--------------------- 1 file changed, 128 insertions(+), 120 deletions(-) diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index e48e74a53d3a..62f9283a0002 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -5,10 +5,10 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7" + sha256: eb376e9acf6938204f90eb3b1f00b578640d3188b4c8a8ec054f9f479af8d051 url: "https://pub.dev" source: hosted - version: "67.0.0" + version: "64.0.0" after_layout: dependency: transitive description: @@ -21,10 +21,10 @@ packages: dependency: transitive description: name: analyzer - sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d" + sha256: "69f54f967773f6c26c7dcb13e93d7ccee8b17a641689da39e878d5cf13b06893" url: "https://pub.dev" source: hosted - version: "6.4.1" + version: "6.2.0" animations: dependency: transitive description: @@ -37,18 +37,18 @@ packages: dependency: transitive description: name: archive - sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d + sha256: "22600aa1e926be775fa5fe7e6894e7fb3df9efda8891c73f70fb3262399a432d" url: "https://pub.dev" source: hosted - version: "3.6.1" + version: "3.4.10" args: dependency: transitive description: name: args - sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a" + sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596 url: "https://pub.dev" source: hosted - version: "2.5.0" + version: "2.4.2" async: dependency: transitive description: @@ -69,10 +69,10 @@ packages: dependency: "direct main" description: name: auto_size_text_field - sha256: "41c90b2270e38edc6ce5c02e5a17737a863e65e246bdfc94565a38f3ec399144" + sha256: d47c81ffa9b61d219f6c50492dc03ea28fa9346561b2ec33b46ccdc000ddb0aa url: "https://pub.dev" source: hosted - version: "2.2.4" + version: "2.2.2" back_button_interceptor: dependency: "direct main" description: @@ -141,10 +141,10 @@ packages: dependency: "direct dev" description: name: build_runner - sha256: "3ac61a79bfb6f6cc11f693591063a7f19a7af628dc52f141743edac5c16e8c22" + sha256: "581bacf68f89ec8792f5e5a0b2c4decd1c948e97ce659dc783688c8a88fbec21" url: "https://pub.dev" source: hosted - version: "2.4.9" + version: "2.4.8" build_runner_core: dependency: transitive description: @@ -165,10 +165,10 @@ packages: dependency: transitive description: name: built_value - sha256: c7913a9737ee4007efedaffc968c049fd0f3d0e49109e778edc10de9426005cb + sha256: a3ec2e0f967bc47f69f95009bb93db936288d61d5343b9436e378b28a2f830c6 url: "https://pub.dev" source: hosted - version: "8.9.2" + version: "8.9.0" cached_network_image: dependency: transitive description: @@ -189,10 +189,10 @@ packages: dependency: transitive description: name: cached_network_image_web - sha256: "205d6a9f1862de34b93184f22b9d2d94586b2f05c581d546695e3d8f6a805cd7" + sha256: "42a835caa27c220d1294311ac409a43361088625a4f23c820b006dd9bffb3316" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.1.1" characters: dependency: transitive description: @@ -269,10 +269,10 @@ packages: dependency: transitive description: name: cross_file - sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670" + sha256: fedaadfa3a6996f75211d835aaeb8fede285dae94262485698afd832371b9a5e url: "https://pub.dev" source: hosted - version: "0.3.4+2" + version: "0.3.3+8" crypto: dependency: transitive description: @@ -293,10 +293,10 @@ packages: dependency: transitive description: name: dart_style - sha256: "99e066ce75c89d6b29903d788a7bb9369cf754f7b24bf70bf4b6d6d6b26853b9" + sha256: "40ae61a5d43feea6d24bd22c0537a6629db858963b99b4bc1c3db80676f32368" url: "https://pub.dev" source: hosted - version: "2.3.6" + version: "2.3.4" dash_chat_2: dependency: "direct main" description: @@ -351,10 +351,10 @@ packages: dependency: transitive description: name: device_info_plus_platform_interface - sha256: "282d3cf731045a2feb66abfe61bbc40870ae50a3ed10a4d3d217556c35c8c2ba" + sha256: d3b01d5868b50ae571cd1dc6e502fc94d956b665756180f7b16ead09e836fd64 url: "https://pub.dev" source: hosted - version: "7.0.1" + version: "7.0.0" draggable_float_widget: dependency: "direct main" description: @@ -408,10 +408,10 @@ packages: dependency: "direct main" description: name: ffi - sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" + sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878" url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.1.0" ffigen: dependency: "direct dev" description: @@ -448,10 +448,10 @@ packages: dependency: transitive description: name: file_selector_macos - sha256: f42eacb83b318e183b1ae24eead1373ab1334084404c8c16e0354f9a3e55d385 + sha256: b15c3da8bd4908b9918111fa486903f5808e388b8d1c559949f584725a6594d6 url: "https://pub.dev" source: hosted - version: "0.9.4" + version: "0.9.3+3" file_selector_platform_interface: dependency: transitive description: @@ -464,10 +464,10 @@ packages: dependency: transitive description: name: file_selector_windows - sha256: "2ad726953f6e8affbc4df8dc78b77c3b4a060967a291e528ef72ae846c60fb69" + sha256: d3547240c20cabf205c7c7f01a50ecdbc413755814d6677f3cb366f04abcead0 url: "https://pub.dev" source: hosted - version: "0.9.3+2" + version: "0.9.3+1" fixnum: dependency: transitive description: @@ -480,18 +480,18 @@ packages: dependency: "direct main" description: name: flex_color_picker - sha256: "5c846437069fb7afdd7ade6bf37e628a71d2ab0787095ddcb1253bf9345d5f3a" + sha256: "0871edc170153cfc3de316d30625f40a85daecfa76ce541641f3cc0ec7757cbf" url: "https://pub.dev" source: hosted - version: "3.4.1" + version: "3.3.1" flex_seed_scheme: dependency: transitive description: name: flex_seed_scheme - sha256: "4cee2f1d07259f77e8b36f4ec5f35499d19e74e17c7dce5b819554914082bc01" + sha256: "29c12aba221eb8a368a119685371381f8035011d18de5ba277ad11d7dfb8657f" url: "https://pub.dev" source: hosted - version: "1.5.0" + version: "1.4.0" flutter: dependency: "direct main" description: flutter @@ -620,10 +620,10 @@ packages: dependency: transitive description: name: flutter_plugin_android_lifecycle - sha256: "8cf40eebf5dec866a6d1956ad7b4f7016e6c0cc69847ab946833b7d43743809f" + sha256: b068ffc46f82a55844acfa4fdbb61fad72fa2aef0905548419d97f0f95c456da url: "https://pub.dev" source: hosted - version: "2.0.19" + version: "2.0.17" flutter_rust_bridge: dependency: "direct main" description: @@ -636,10 +636,10 @@ packages: dependency: "direct main" description: name: flutter_svg - sha256: "7b4ca6cf3304575fe9c8ec64813c8d02ee41d2afe60bcfe0678bcb5375d596a2" + sha256: d39e7f95621fc84376bc0f7d504f05c3a41488c562f4a8ad410569127507402c url: "https://pub.dev" source: hosted - version: "2.0.10+1" + version: "2.0.9" flutter_web_plugins: dependency: transitive description: flutter @@ -649,26 +649,26 @@ packages: dependency: "direct dev" description: name: freezed - sha256: a434911f643466d78462625df76fd9eb13e57348ff43fe1f77bbe909522c67a1 + sha256: "57247f692f35f068cae297549a46a9a097100685c6780fe67177503eea5ed4e5" url: "https://pub.dev" source: hosted - version: "2.5.2" + version: "2.4.7" freezed_annotation: dependency: "direct main" description: name: freezed_annotation - sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2 + sha256: c3fd9336eb55a38cc1bbd79ab17573113a8deccd0ecbbf926cca3c62803b5c2d url: "https://pub.dev" source: hosted - version: "2.4.4" + version: "2.4.1" frontend_server_client: dependency: transitive description: name: frontend_server_client - sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 + sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612" url: "https://pub.dev" source: hosted - version: "4.0.0" + version: "3.2.0" get: dependency: "direct main" description: @@ -705,10 +705,10 @@ packages: dependency: "direct main" description: name: http - sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010 + sha256: a2bbf9d017fcced29139daa8ed2bba4ece450ab222871df93ca9eec6f80c34ba url: "https://pub.dev" source: hosted - version: "1.2.2" + version: "1.2.0" http_multi_server: dependency: transitive description: @@ -737,10 +737,10 @@ packages: dependency: "direct main" description: name: image - sha256: "2237616a36c0d69aef7549ab439b833fb7f9fb9fc861af2cc9ac3eedddd69ca8" + sha256: "4c68bfd5ae83e700b5204c1e74451e7bf3cf750e6843c6e158289cf56bda018e" url: "https://pub.dev" source: hosted - version: "4.2.0" + version: "4.1.7" image_picker: dependency: "direct main" description: @@ -753,10 +753,10 @@ packages: dependency: transitive description: name: image_picker_android - sha256: "0f57fee1e8bfadf8cc41818bbcd7f72e53bb768a54d9496355d5e8a5681a19f1" + sha256: "39f2bfe497e495450c81abcd44b62f56c2a36a37a175da7d137b4454977b51b1" url: "https://pub.dev" source: hosted - version: "0.8.12+1" + version: "0.8.9+3" image_picker_for_web: dependency: transitive description: @@ -769,10 +769,10 @@ packages: dependency: transitive description: name: image_picker_ios - sha256: "6703696ad49f5c3c8356d576d7ace84d1faf459afb07accbb0fae780753ff447" + sha256: fadafce49e8569257a0cad56d24438a6fa1f0cbd7ee0af9b631f7492818a4ca3 url: "https://pub.dev" source: hosted - version: "0.8.12" + version: "0.8.9+1" image_picker_linux: dependency: transitive description: @@ -793,10 +793,10 @@ packages: dependency: transitive description: name: image_picker_platform_interface - sha256: "9ec26d410ff46f483c5519c29c02ef0e02e13a543f882b152d4bfd2f06802f80" + sha256: fa4e815e6fcada50e35718727d83ba1c92f1edf95c0b4436554cec301b56233b url: "https://pub.dev" source: hosted - version: "2.10.0" + version: "2.9.3" image_picker_windows: dependency: transitive description: @@ -833,10 +833,10 @@ packages: dependency: transitive description: name: json_annotation - sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" + sha256: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467 url: "https://pub.dev" source: hosted - version: "4.9.0" + version: "4.8.1" lints: dependency: transitive description: @@ -881,10 +881,10 @@ packages: dependency: transitive description: name: mime - sha256: "801fd0b26f14a4a58ccb09d5892c3fbdeff209594300a542492cf13fba9d247a" + sha256: "2e123074287cc9fd6c09de8336dae606d1ddb88d9ac47358826db698c176a1f2" url: "https://pub.dev" source: hosted - version: "1.0.6" + version: "1.0.5" nested: dependency: transitive description: @@ -897,10 +897,10 @@ packages: dependency: transitive description: name: octo_image - sha256: "34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd" + sha256: "45b40f99622f11901238e18d48f5f12ea36426d8eced9f4cbf58479c7aa2430d" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.0.0" package_config: dependency: transitive description: @@ -953,26 +953,26 @@ packages: dependency: "direct main" description: name: path_provider - sha256: fec0d61223fba3154d87759e3cc27fe2c8dc498f6386c6d6fc80d1afdd1bf378 + sha256: b27217933eeeba8ff24845c34003b003b2b22151de3c908d0e679e8fe1aa078b url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.1.2" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: a248d8146ee5983446bf03ed5ea8f6533129a12b11f12057ad1b4a67a2b3b41d + sha256: "477184d672607c0a3bf68fbbf601805f92ef79c82b64b4d6eb318cbca4c48668" url: "https://pub.dev" source: hosted - version: "2.2.4" + version: "2.2.2" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: f234384a3fdd67f989b4d54a5d73ca2a6c422fa55ae694381ae0f4375cd1ea16 + sha256: "5a7999be66e000916500be4f15a3633ebceb8302719b47b9cc49ce924125350f" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.3.2" path_provider_linux: dependency: transitive description: @@ -993,10 +993,10 @@ packages: dependency: transitive description: name: path_provider_windows - sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.2.1" pedantic: dependency: transitive description: @@ -1025,10 +1025,10 @@ packages: dependency: transitive description: name: platform - sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" + sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" url: "https://pub.dev" source: hosted - version: "3.1.5" + version: "3.1.4" plugin_platform_interface: dependency: transitive description: @@ -1037,6 +1037,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.8" + pointycastle: + dependency: transitive + description: + name: pointycastle + sha256: "43ac87de6e10afabc85c445745a7b799e04de84cebaa4fd7bf55a5e1e9604d29" + url: "https://pub.dev" + source: hosted + version: "3.7.4" pool: dependency: transitive description: @@ -1049,10 +1057,10 @@ packages: dependency: "direct main" description: name: provider - sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c + sha256: "9a96a0a19b594dbc5bf0f1f27d2bc67d5f95957359b461cd9feb44ed6ae75096" url: "https://pub.dev" source: hosted - version: "6.1.2" + version: "6.1.1" pub_semver: dependency: transitive description: @@ -1065,26 +1073,26 @@ packages: dependency: transitive description: name: pubspec_parse - sha256: c799b721d79eb6ee6fa56f00c04b472dcd44a30d258fac2174a6ec57302678f8 + sha256: c63b2876e58e194e4b0828fcb080ad0e06d051cb607a6be51a9e084f47cb9367 url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.2.3" pull_down_button: dependency: "direct main" description: name: pull_down_button - sha256: "48b928203afdeafa4a8be5dc96980523bc8a2ddbd04569f766071a722be22379" + sha256: "235b302701ce029fd9e9470975069376a6700935bb47a5f1b3ec8a5efba07e6f" url: "https://pub.dev" source: hosted - version: "0.9.4" + version: "0.9.3" puppeteer: dependency: transitive description: name: puppeteer - sha256: a6752d4f09b510ae41911bfd0997f957e723d38facf320dd9ee0e5661108744a + sha256: eedeaae6ec5d2e54f9ae22ab4d6b3dda2e8791c356cc783046d06c287ffe11d8 url: "https://pub.dev" source: hosted - version: "3.13.0" + version: "3.6.0" qr: dependency: transitive description: @@ -1113,10 +1121,10 @@ packages: dependency: transitive description: name: quiver - sha256: ea0b925899e64ecdfbf9c7becb60d5b50e706ade44a85b2363be2a22d88117d2 + sha256: b1c1ac5ce6688d77f65f3375a9abb9319b3cb32486bdc7a1e0fdf004d7ba4e47 url: "https://pub.dev" source: hosted - version: "3.2.2" + version: "3.2.1" rxdart: dependency: transitive description: @@ -1161,10 +1169,10 @@ packages: dependency: transitive description: name: shelf_static - sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3 + sha256: a41d3f53c4adf0f57480578c1d61d90342cd617de7fc8077b1304643c2d85c1e url: "https://pub.dev" source: hosted - version: "1.1.3" + version: "1.1.2" shelf_web_socket: dependency: transitive description: @@ -1206,18 +1214,18 @@ packages: dependency: transitive description: name: sqflite - sha256: a43e5a27235518c03ca238e7b4732cf35eabe863a369ceba6cbefa537a66f16d + sha256: a9016f495c927cb90557c909ff26a6d92d9bd54fc42ba92e19d4e79d61e798c6 url: "https://pub.dev" source: hosted - version: "2.3.3+1" + version: "2.3.2" sqflite_common: dependency: transitive description: name: sqflite_common - sha256: "3da423ce7baf868be70e2c0976c28a1bb2f73644268b7ffa7d2e08eab71f16a4" + sha256: "28d8c66baee4968519fb8bd6cdbedad982d6e53359091f0b74544a9f32ec72d5" url: "https://pub.dev" source: hosted - version: "2.5.4" + version: "2.5.3" stack_trace: dependency: transitive description: @@ -1246,10 +1254,10 @@ packages: dependency: transitive description: name: string_scanner - sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3" + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.2.0" synchronized: dependency: transitive description: @@ -1294,10 +1302,10 @@ packages: dependency: "direct main" description: name: toggle_switch - sha256: dca04512d7c23ed320d6c5ede1211a404f177d54d353bf785b07d15546a86ce5 + sha256: "9e6af1f0c5a97d9de41109dc7b9e1b3bbe73417f89b10e0e44dc834fb493d4cb" url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.1.0" tuple: dependency: "direct main" description: @@ -1406,10 +1414,10 @@ packages: dependency: transitive description: name: url_launcher_web - sha256: "772638d3b34c779ede05ba3d38af34657a05ac55b06279ea6edd409e323dca8e" + sha256: fff0932192afeedf63cdd50ecbb1bc825d31aed259f02bb8dba0f3b729a5e88b url: "https://pub.dev" source: hosted - version: "2.3.3" + version: "2.2.3" url_launcher_windows: dependency: transitive description: @@ -1430,26 +1438,26 @@ packages: dependency: transitive description: name: vector_graphics - sha256: "32c3c684e02f9bc0afb0ae0aa653337a2fe022e8ab064bcd7ffda27a74e288e3" + sha256: "4ac59808bbfca6da38c99f415ff2d3a5d7ca0a6b4809c71d9cf30fba5daf9752" url: "https://pub.dev" source: hosted - version: "1.1.11+1" + version: "1.1.10+1" vector_graphics_codec: dependency: transitive description: name: vector_graphics_codec - sha256: c86987475f162fadff579e7320c7ddda04cd2fdeffbe1129227a85d9ac9e03da + sha256: f3247e7ab0ec77dc759263e68394990edc608fb2b480b80db8aa86ed09279e33 url: "https://pub.dev" source: hosted - version: "1.1.11+1" + version: "1.1.10+1" vector_graphics_compiler: dependency: transitive description: name: vector_graphics_compiler - sha256: "12faff3f73b1741a36ca7e31b292ddeb629af819ca9efe9953b70bd63fc8cd81" + sha256: "18489bdd8850de3dd7ca8a34e0c446f719ec63e2bab2e7a8cc66a9028dd76c5a" url: "https://pub.dev" source: hosted - version: "1.1.11+1" + version: "1.1.10+1" vector_math: dependency: transitive description: @@ -1462,26 +1470,26 @@ packages: dependency: transitive description: name: video_player - sha256: e30df0d226c4ef82e2c150ebf6834b3522cf3f654d8e2f9419d376cdc071425d + sha256: fbf28ce8bcfe709ad91b5789166c832cb7a684d14f571a81891858fefb5bb1c2 url: "https://pub.dev" source: hosted - version: "2.9.1" + version: "2.8.2" video_player_android: dependency: transitive description: name: video_player_android - sha256: "134e1ad410d67e18a19486ed9512c72dfc6d8ffb284d0e8f2e99e903d1ba8fa3" + sha256: "7f8f25d7ad56819a82b2948357f3c3af071f6a678db33833b26ec36bbc221316" url: "https://pub.dev" source: hosted - version: "2.4.14" + version: "2.4.11" video_player_avfoundation: dependency: transitive description: name: video_player_avfoundation - sha256: d1e9a824f2b324000dc8fb2dcb2a3285b6c1c7c487521c63306cc5b394f68a7c + sha256: "309e3962795e761be010869bae65c0b0e45b5230c5cee1bec72197ca7db040ed" url: "https://pub.dev" source: hosted - version: "2.6.1" + version: "2.5.6" video_player_platform_interface: dependency: transitive description: @@ -1494,10 +1502,10 @@ packages: dependency: transitive description: name: video_player_web - sha256: "6dcdd298136523eaf7dfc31abaf0dfba9aa8a8dbc96670e87e9d42b6f2caf774" + sha256: "34beb3a07d4331a24f7e7b2f75b8e2b103289038e07e65529699a671b6a6e2cb" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.1.3" visibility_detector: dependency: "direct main" description: @@ -1510,18 +1518,18 @@ packages: dependency: "direct main" description: name: wakelock_plus - sha256: "104d94837bb28c735894dcd592877e990149c380e6358b00c04398ca1426eed4" + sha256: f268ca2116db22e57577fb99d52515a24bdc1d570f12ac18bb762361d43b043d url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.1.4" wakelock_plus_platform_interface: dependency: transitive description: name: wakelock_plus_platform_interface - sha256: "422d1cdbb448079a8a62a5a770b69baa489f8f7ca21aef47800c726d404f9d16" + sha256: "40fabed5da06caff0796dc638e1f07ee395fb18801fbff3255a2372db2d80385" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.1.0" watcher: dependency: transitive description: @@ -1534,34 +1542,34 @@ packages: dependency: transitive description: name: web - sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27" + sha256: "4188706108906f002b3a293509234588823c8c979dc83304e229ff400c996b05" url: "https://pub.dev" source: hosted - version: "0.5.1" + version: "0.4.2" web_socket_channel: dependency: transitive description: name: web_socket_channel - sha256: "58c6666b342a38816b2e7e50ed0f1e261959630becd4c879c4f26bfa14aa5a42" + sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b url: "https://pub.dev" source: hosted - version: "2.4.5" + version: "2.4.0" win32: dependency: "direct main" description: name: win32 - sha256: "0eaf06e3446824099858367950a813472af675116bf63f008a4c2a75ae13e9cb" + sha256: "464f5674532865248444b4c3daca12bd9bf2d7c47f759ce2617986e7229494a8" url: "https://pub.dev" source: hosted - version: "5.5.0" + version: "5.2.0" win32_registry: dependency: transitive description: name: win32_registry - sha256: "10589e0d7f4e053f2c61023a31c9ce01146656a70b7b7f0828c0b46d7da2a9bb" + sha256: "41fd8a189940d8696b1b810efb9abcf60827b6cbfab90b0c43e8439e3a39d85a" url: "https://pub.dev" source: hosted - version: "1.1.3" + version: "1.1.2" window_manager: dependency: "direct main" description: @@ -1608,18 +1616,18 @@ packages: dependency: transitive description: name: yaml_edit - sha256: e9c1a3543d2da0db3e90270dbb1e4eebc985ee5e3ffe468d83224472b2194a5f + sha256: "1579d4a0340a83cf9e4d580ea51a16329c916973bffd5bd4b45e911b25d46bfd" url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.1.1" zxing2: dependency: "direct main" description: name: zxing2 - sha256: "6cf995abd3c86f01ba882968dedffa7bc130185e382f2300239d2e857fc7912c" + sha256: a042961441bd400f59595f9125ef5fca4c888daf0ea59c17f41e0e151f8a12b5 url: "https://pub.dev" source: hosted - version: "0.2.3" + version: "0.2.1" sdks: dart: ">=3.3.0 <4.0.0" flutter: ">=3.19.0" From 519539ed0a35c9692192d8f2f50709f6c18c0c33 Mon Sep 17 00:00:00 2001 From: Gheorghi Date: Tue, 10 Sep 2024 18:22:14 +0300 Subject: [PATCH 178/541] Update bg.rs with more translations (#9317) --- src/lang/bg.rs | 682 ++++++++++++++++++++++++------------------------- 1 file changed, 341 insertions(+), 341 deletions(-) diff --git a/src/lang/bg.rs b/src/lang/bg.rs index 9a5b320a85d0..7efdd0dfa749 100644 --- a/src/lang/bg.rs +++ b/src/lang/bg.rs @@ -1,19 +1,19 @@ lazy_static::lazy_static! { pub static ref T: std::collections::HashMap<&'static str, &'static str> = [ - ("Status", "Статус"), - ("Your Desktop", "Твоят Работен Плот"), - ("desk_tip", "Вашият работен плот може да бъде достъпен с този идентификационен код и парола."), + ("Status", "Положение"), + ("Your Desktop", "Вашата работна среда"), + ("desk_tip", "Вашата работна среда не може да бъде достъпена с този потребителски код и парола."), ("Password", "Парола"), ("Ready", "Готово"), ("Established", "Установен"), ("connecting_status", "Свързване с RustDesk мрежата..."), - ("Enable service", "Пусни услуга"), + ("Enable service", "Разреши услуга"), ("Start service", "Стартирай услуга"), ("Service is running", "Услугата работи"), ("Service is not running", "Услугата не работи"), ("not_ready_status", "Не е в готовност. Моля проверете мрежова връзка"), - ("Control Remote Desktop", "Контролирайте отдалечения работен плот"), + ("Control Remote Desktop", "Отдалечено управление на работна среда"), ("Transfer file", "Прехвърляне на файл"), ("Connect", "Свързване"), ("Recent sessions", "Последни сесии"), @@ -23,27 +23,27 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Remove", "Премахване"), ("Refresh random password", "Опресняване на произволна парола"), ("Set your own password", "Задайте собствена парола"), - ("Enable keyboard/mouse", "Разрешение на клавиатура/мишка"), - ("Enable clipboard", "Разрешение на клипборда"), - ("Enable file transfer", "Разрешение прехвърлянето на файлове"), - ("Enable TCP tunneling", "Разрешение за TCP тунел"), - ("IP Whitelisting", "IP беял списък"), - ("ID/Relay Server", "ID/Релейн сървър"), - ("Import server config", "Експортиране конфигурацията на сървъра"), - ("Export Server Config", "Експортиране на конфигурация на сървъра"), - ("Import server configuration successfully", "Импортирането конфигурацията на сървъра успешно"), - ("Export server configuration successfully", "Експортирането конфигурацията на сървъра успешно"), - ("Invalid server configuration", "Невалидна конфигурация на сървъра"), + ("Enable keyboard/mouse", "Позволяване на клавиатура/мишка"), + ("Enable clipboard", "Позволяване достъп до клипборда"), + ("Enable file transfer", "Позволяване прехвърляне на файлове"), + ("Enable TCP tunneling", "Позволяване на TCP тунели"), + ("IP Whitelisting", "Определяне на позволени IP по списък"), + ("ID/Relay Server", "ID/Препредаващ сървър"), + ("Import server config", "Внасяне сървър настройки за "), + ("Export Server Config", "Изнасяне настройки на сървър"), + ("Import server configuration successfully", "Успешно внасяне на сървърни настройки"), + ("Export server configuration successfully", "Успешно изнасяне на сървърни настройки"), + ("Invalid server configuration", "Недопустими сървърни настройки"), ("Clipboard is empty", "Клипбордът е празен"), - ("Stop service", "Спрете услугата"), - ("Change ID", "Промяна на ID"), - ("Your new ID", "Вашето ново ID"), + ("Stop service", "Спираане на услуга"), + ("Change ID", "Промяна определител (ID)"), + ("Your new ID", "Вашият нов определител (ID)"), ("length %min% to %max%", "дължина %min% до %max%"), ("starts with a letter", "започва с буква"), ("allowed characters", "разрешени знаци"), - ("id_change_tip", "Само a-z, A-Z, 0-9 и _ (долна черта) символи са позволени. Първата буква трябва да е a-z, A-Z. С дължина мержу 6 и 16."), + ("id_change_tip", "Само a-z, A-Z, 0-9 и _ (долна черта) са сред позволени. Първа буква следва да е a-z, A-Z. С дължина мержу 6 и 16."), ("Website", "Уебсайт"), - ("About", "Относно програмата"), + ("About", "Относно"), ("Slogan_tip", "Направено от сърце в този хаотичен свят!"), ("Privacy Statement", "Декларация за поверителност"), ("Mute", "Без звук"), @@ -53,23 +53,23 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Audio Input", "Аудио вход"), ("Enhancements", "Подобрения"), ("Hardware Codec", "Хардуерен кодек"), - ("Adaptive bitrate", "Адаптивен битрейт"), + ("Adaptive bitrate", "Приспособяваще се скорост на предаване наданни"), ("ID Server", "ID сървър"), - ("Relay Server", "Релейн сървър"), + ("Relay Server", "Препращащ сървър"), ("API Server", "API сървър"), ("invalid_http", "трябва да започва с http:// или https://"), - ("Invalid IP", "Невалиден IP"), - ("Invalid format", "Невалиден формат"), + ("Invalid IP", "Недопустим IP"), + ("Invalid format", "Недопустим формат"), ("server_not_support", "Все още не се поддържа от сървъра"), ("Not available", "Не е наличен"), ("Too frequent", "Твърде често"), ("Cancel", "Отказ"), ("Skip", "Пропускане"), - ("Close", "Затвори"), - ("Retry", "Опитайте отново"), + ("Close", "Затваряне"), + ("Retry", "Преповтори"), ("OK", "Добре"), ("Password Required", "Изисква се парола"), - ("Please enter your password", "Моля въведете паролата си"), + ("Please enter your password", "Моля въведете парола"), ("Remember password", "Запомни паролата"), ("Wrong Password", "Грешна парола"), ("Do you want to enter again?", "Искате ли да въведете отново?"), @@ -99,73 +99,73 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Properties", "Свойства"), ("Multi Select", "Множествен избор"), ("Select All", "Избери всички"), - ("Unselect All", "Деселектирай всички"), - ("Empty Directory", "Празна директория"), - ("Not an empty directory", "Не е празна директория"), + ("Unselect All", "Избери никой"), + ("Empty Directory", "Празна папка"), + ("Not an empty directory", "Не е празна папка"), ("Are you sure you want to delete this file?", "Сигурни ли сте, че искате да изтриете този файл?"), - ("Are you sure you want to delete this empty directory?", "Сигурни ли сте, че искате да изтриете тази празна директория?"), - ("Are you sure you want to delete the file of this directory?", "Сигурни ли сте, че искате да изтриете файла от тази директория?"), - ("Do this for all conflicts", "Направете това за всички конфликти"), - ("This is irreversible!", ""), + ("Are you sure you want to delete this empty directory?", "Сигурни ли сте, че искате да изтриете тази празна папка?"), + ("Are you sure you want to delete the file of this directory?", "Сигурни ли сте, че искате да изтриете файла от тази папка?"), + ("Do this for all conflicts", "Разреши така всички конфликти"), + ("This is irreversible!", "Това е необратимо!"), ("Deleting", "Изтриване"), ("files", "файлове"), - ("Waiting", ""), - ("Finished", "Готово"), + ("Waiting", "Изчакване"), + ("Finished", "Завършено"), ("Speed", "Скорост"), - ("Custom Image Quality", "Персонализирано качество на изображението"), + ("Custom Image Quality", "Качество на изображението по свой избор"), ("Privacy mode", "Режим на поверителност"), - ("Block user input", "Блокиране на потребителско въвеждане"), - ("Unblock user input", "Отблокиране на потребителско въвеждане"), - ("Adjust Window", "Регулирай прозореца"), + ("Block user input", "Забрана за потребителски вход"), + ("Unblock user input", "Разрешаване на потребителски въвеждане"), + ("Adjust Window", "Нагласи прозореца"), ("Original", "Оригинално"), ("Shrink", "Свиване"), ("Stretch", "Разтегнат"), ("Scrollbar", "Плъзгач"), - ("ScrollAuto", "Автоматичен плъзгач"), + ("ScrollAuto", "Автоматичено приплъзване"), ("Good image quality", "Добро качество на изображението"), - ("Balanced", "Балансиран"), - ("Optimize reaction time", "Оптимизирайте времето за реакция"), - ("Custom", "Персонализиран"), - ("Show remote cursor", "Показване на дистанционния курсор"), - ("Show quality monitor", "Показване на прозорец за качество"), - ("Disable clipboard", "Деактивиране на клипборда"), - ("Lock after session end", "Заключване след края на сесията"), + ("Balanced", "Уравновесен"), + ("Optimize reaction time", "С оглед времето на реакция"), + ("Custom", "По собствено желание"), + ("Show remote cursor", "Показвай отдалечения курсор"), + ("Show quality monitor", "Показвай прозорец за качество"), + ("Disable clipboard", "Забрана за достъп до клипборд"), + ("Lock after session end", "Заключване след край на ползване"), ("Insert", "Поставяне"), ("Insert Lock", "Заявка за заключване"), - ("Refresh", "Обнови"), - ("ID does not exist", "ID-то не съществува"), - ("Failed to connect to rendezvous server", "Неуспешно свързване със сървъра за рандеву"), + ("Refresh", "Обновяване"), + ("ID does not exist", "Несъществуващ определител (ID)"), + ("Failed to connect to rendezvous server", "Неуспешно свързване към сървъра за среща (rendezvous)"), ("Please try later", "Моля опитайте по-късно"), - ("Remote desktop is offline", "Отдалеченият работен плот е офлайн"), + ("Remote desktop is offline", "Отдалечената работна среда не е налична"), ("Key mismatch", "Ключово несъответствие"), - ("Timeout", ""), - ("Failed to connect to relay server", ""), - ("Failed to connect via rendezvous server", ""), - ("Failed to connect via relay server", ""), - ("Failed to make direct connection to remote desktop", ""), - ("Set Password", "Задайте парола"), + ("Timeout", "Изтичане на времето"), + ("Failed to connect to relay server", "Провал при свързване към препредаващ сървър"), + ("Failed to connect via rendezvous server", "Провал при свързване към сървър за срещи (rendezvous)"), + ("Failed to connect via relay server", "Провал при свързване чрез препредаващ сървър"), + ("Failed to make direct connection to remote desktop", "Провал при установяване на пряка връзка с отдалечена работна среда"), + ("Set Password", "Задаване на парола"), ("OS Password", "Парола на Операционната система"), - ("install_tip", "Поради UAC, RustDesk в някои случай не може да работи правилно като отдалечена достъп. За да заобиколите UAC, моля, щракнете върху бутона по-долу, за да инсталирате RustDesk в системата."), - ("Click to upgrade", "Кликнете, за да надстроите"), - ("Click to download", "Кликнете, за да изтеглите"), - ("Click to update", "Кликнете, за да актуализирате"), - ("Configure", "Конфигуриране"), - ("config_acc", "За да управлявате вашия работен плот дистанционно, трябва да предоставите на RustDesk разрешения \"Достъпност\"."), - ("config_screen", "In order to access your Desktop remotely, you need to grant RustDesk \"Screen Recording\" permissions."), - ("Installing ...", "Инсталиране..."), - ("Install", "Инсталирай"), - ("Installation", "Инсталация"), - ("Installation Path", "Инсталационен път"), - ("Create start menu shortcuts", "Създайте преки пътища в менюто 'Старт'."), + ("install_tip", "Поради UAC, RustDesk в някои случай не може да работи правилно за отдалечена достъп. За да заобиколите UAC, моля, натиснете копчето по-долу, за да поставите RustDesk като системна услуга."), + ("Click to upgrade", "Натиснете, за да надстроите"), + ("Click to download", "Натиснете, за да изтеглите"), + ("Click to update", "Натиснете, за да обновите"), + ("Configure", "Настройване"), + ("config_acc", "За да управлявате вашия работна среда отдалечено, трябва да предоставите на RustDesk права от раздел \"Достъпност\"."), + ("config_screen", "За да управлявате вашия работна среда отдалечено, трябва да предоставите на RustDesk права от раздел \"Запис на екрана\"."), + ("Installing ...", "Поставяне..."), + ("Install", "Постави"), + ("Installation", "Поставяне"), + ("Installation Path", "Път към място за поставяне"), + ("Create start menu shortcuts", "Бърз достъп от меню 'Старт'."), ("Create desktop icon", "Създайте икона на работния плот"), - ("agreement_tip", "Стартирайки инсталацията, вие приемате лицензионното споразумение."), - ("Accept and Install", "Приемете и инсталирайте"), - ("End-user license agreement", ""), - ("Generating ...", "Генериране..."), + ("agreement_tip", "Започвайки поставянето, вие приемате лицензионното споразумение."), + ("Accept and Install", "Приемете и поставяте"), + ("End-user license agreement", "Споразумение с потребителя"), + ("Generating ...", "Пораждане..."), ("Your installation is lower version.", "Вашата инсталация е по-ниска версия."), ("not_close_tcp_tip", "Не затваряйте този прозорец, докато използвате тунела"), ("Listening ...", "Слушане..."), - ("Remote Host", "Отдалечен хост"), + ("Remote Host", "Отдалечен сървър"), ("Remote Port", "Отдалечен порт"), ("Action", "Действие"), ("Add", "Добави"), @@ -173,154 +173,154 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Local Address", "Локален адрес"), ("Change Local Port", "Промяна на локалният порт"), ("setup_server_tip", "За по-бърза връзка, моля направете свой собствен сървър"), - ("Too short, at least 6 characters.", ""), - ("The confirmation is not identical.", ""), + ("Too short, at least 6 characters.", "Прекалено кратко, поне 6 знака"), + ("The confirmation is not identical.", "Потвърждението не съвпада"), ("Permissions", "Разрешения"), ("Accept", "Приеми"), ("Dismiss", "Отхвърляне"), - ("Disconnect", "Прекъснете връзката"), - ("Enable file copy and paste", ""), + ("Disconnect", "Прекъсване"), + ("Enable file copy and paste", "Разрешаване прехвърляне на файлове"), ("Connected", "Свързан"), - ("Direct and encrypted connection", "Директна и криптирана връзка"), - ("Relayed and encrypted connection", "Препредадена и криптирана връзка"), - ("Direct and unencrypted connection", "Директна и некриптирана връзка"), - ("Relayed and unencrypted connection", "Препредадена и некриптирана връзка"), - ("Enter Remote ID", "Въведете дистанционно ID"), - ("Enter your password", "Въведете паролата си"), - ("Logging in...", ""), - ("Enable RDP session sharing", "Активирайте споделянето на RDP сесия"), + ("Direct and encrypted connection", "Пряка защитена връзка"), + ("Relayed and encrypted connection", "Препредадена защитена връзка"), + ("Direct and unencrypted connection", "Пряка незащитена връзка"), + ("Relayed and unencrypted connection", "Препредадена незащитена връзка"), + ("Enter Remote ID", "Въведете отдалеченото ID"), + ("Enter your password", "Въведете парола"), + ("Logging in...", "Вписване..."), + ("Enable RDP session sharing", "Позволяване споделянето на RDP сесия"), ("Auto Login", "Автоматично вписване (Валидно само ако зададете \"Заключване след края на сесията\")"), - ("Enable direct IP access", "Разрешете директен IP достъп"), + ("Enable direct IP access", "Разрешаване пряк IP достъп"), ("Rename", "Преименуване"), ("Space", "Пространство"), ("Create desktop shortcut", "Създайте пряк път на работния плот"), ("Change Path", "Промяна на пътя"), ("Create Folder", "Създай папка"), - ("Please enter the folder name", "Моля, въведете името на папката"), + ("Please enter the folder name", "Моля, въведете име на папката"), ("Fix it", "Оправи го"), ("Warning", "Внимание"), - ("Login screen using Wayland is not supported", "Екранът за влизане с помощта на Wayland не се поддържа"), - ("Reboot required", "Изисква се рестартиране"), - ("Unsupported display server", "Неподдържан сървър за дисплея"), - ("x11 expected", ""), + ("Login screen using Wayland is not supported", "Екран за влизане чрез Wayland не се поддържа"), + ("Reboot required", "Нужно е презареждане на ОС"), + ("Unsupported display server", "Неподдържан екранен сървър"), + ("x11 expected", "Очаква се x11"), ("Port", "Порт"), ("Settings", "Настройки"), ("Username", "Потребителско име"), - ("Invalid port", "Невалиден порт"), - ("Closed manually by the peer", "Затворено ръчно от партньора"), + ("Invalid port", "Недопустим порт"), + ("Closed manually by the peer", "Затворено ръчно от другата страна"), ("Enable remote configuration modification", "Разрешаване на отдалечена промяна на конфигурацията"), ("Run without install", "Стартирайте без инсталиране"), - ("Connect via relay", "Свържете чрез реле"), - ("Always connect via relay", "Винаги свързвайте чрез реле"), + ("Connect via relay", "Свързване чрез препращане"), + ("Always connect via relay", "Винаги чрез препращане"), ("whitelist_tip", "Само IP адресите от белия списък имат достъп до мен"), ("Login", "Влизане"), ("Verify", "Потвърди"), ("Remember me", "Запомни ме"), - ("Trust this device", "Доверете се на това устройство"), + ("Trust this device", "Доверяване на това устройство"), ("Verification code", "Код за потвърждение"), - ("verification_tip", "На регистрирания имейл адрес е изпратен код за потвърждение, въведете кода за потвърждение, за да продължите да влизате."), - ("Logout", "Излез от профила си"), - ("Tags", "Етикети"), - ("Search ID", "Търсене на ID"), - ("whitelist_sep", "Разделени със запетая, точка и запетая, интервали или нов ред"), + ("verification_tip", "На посочения имейл е изпратен код за потвърждение. Моля въведете го, за да продължите с влизането."), + ("Logout", "Отписване (Изход)"), + ("Tags", "Белези"), + ("Search ID", "Търси ID"), + ("whitelist_sep", "Разделени със запетая, точка и запетая, празни символи или нов ред"), ("Add ID", "Добави ID"), ("Add Tag", "Добави етикет"), - ("Unselect all tags", "Премахнете избора на всички етикети"), + ("Unselect all tags", "Премахнете избора на всички белези (tags)"), ("Network error", "Мрежова грешка"), - ("Username missed", "Пропуснато потребителско име"), - ("Password missed", "Пропусната парола"), - ("Wrong credentials", "Wrong username or password"), - ("The verification code is incorrect or has expired", ""), - ("Edit Tag", "Edit tag"), + ("Username missed", "Липсващо потребителско име"), + ("Password missed", "Липсваща парола"), + ("Wrong credentials", "Грешни пълномощия"), + ("The verification code is incorrect or has expired", "Кодът за проверка е неправилен или с изтекла давност."), + ("Edit Tag", "Промени белег"), ("Forget Password", "Забравена парола"), - ("Favorites", ""), + ("Favorites", "Любими"), ("Add to Favorites", "Добави към любими"), ("Remove from Favorites", "Премахване от любими"), ("Empty", "Празно"), - ("Invalid folder name", ""), - ("Socks5 Proxy", "Socks5 прокси"), - ("Socks5/Http(s) Proxy", "Socks5/Http(s) прокси"), - ("Discovered", ""), - ("install_daemon_tip", "За стартиране с компютъра трябва да инсталирате системна услуга."), - ("Remote ID", "Дистанционно ID"), + ("Invalid folder name", "Непозволено име на папка"), + ("Socks5 Proxy", "Socks5 посредник"), + ("Socks5/Http(s) Proxy", "Socks5/Http(s) посредник"), + ("Discovered", "Открит"), + ("install_daemon_tip", "За зареждане при стартиране на ОС следва да поставите RustDesk като системна услуга."), + ("Remote ID", "Отдалечено ID"), ("Paste", "Постави"), ("Paste here?", "Постави тук?"), ("Are you sure to close the connection?", "Сигурни ли сте, че искате да затворите връзката?"), - ("Download new version", ""), - ("Touch mode", "Режим тъч (сензорен)"), + ("Download new version", "Изтегляне на нова версия"), + ("Touch mode", "Режим сензорен (touch)"), ("Mouse mode", "Режим мишка"), - ("One-Finger Tap", "Докосване с един пръст"), + ("One-Finger Tap", "Допир с един пръст"), ("Left Mouse", "Ляв бутон на мишката"), - ("One-Long Tap", "Едно дълго докосване"), - ("Two-Finger Tap", "Докосване с два пръста"), + ("One-Long Tap", "Дълъг допир"), + ("Two-Finger Tap", "Допир с два пръста"), ("Right Mouse", "Десен бутон на мишката"), ("One-Finger Move", "Преместване с един пръст"), - ("Double Tap & Move", "Докоснете два пъти и преместете"), - ("Mouse Drag", "Плъзгане с мишката"), + ("Double Tap & Move", "Двоен допир и преместване"), + ("Mouse Drag", "Провличане с мишката"), ("Three-Finger vertically", "Три пръста вертикално"), ("Mouse Wheel", "Колело на мишката"), ("Two-Finger Move", "Движение с два пръста"), ("Canvas Move", "Преместване на платното"), ("Pinch to Zoom", "Щипнете, за да увеличите"), ("Canvas Zoom", "Увеличение на платното"), - ("Reset canvas", ""), - ("No permission of file transfer", ""), - ("Note", ""), - ("Connection", ""), + ("Reset canvas", "Нулиране на платното"), + ("No permission of file transfer", "Няма разрешение за прехвърляне на файлове"), + ("Note", "Бележка"), + ("Connection", "Връзка"), ("Share Screen", "Сподели екран"), - ("Chat", "Чат"), - ("Total", "Обшо"), - ("items", "елементи"), + ("Chat", "Говор"), + ("Total", "Общо"), + ("items", "неща"), ("Selected", "Избрано"), - ("Screen Capture", "Заснемане на екрана"), - ("Input Control", "Контрол на въвеждане"), - ("Audio Capture", "Аудио записване"), + ("Screen Capture", "Снемане на екрана"), + ("Input Control", "Управление на вход"), + ("Audio Capture", "Аудиозапис"), ("File Connection", "Файлова връзка"), - ("Screen Connection", "Свързване на екрана"), + ("Screen Connection", "Екранна връзка"), ("Do you accept?", "Приемате ли?"), - ("Open System Setting", "Отворете системната настройка"), - ("How to get Android input permission?", ""), - ("android_input_permission_tip1", "За да може отдалечено устройство да управлява вашето Android устройство чрез мишка или докосване, трябва да разрешите на RustDesk да използва услугата \"Достъпност\"."), + ("Open System Setting", "Отворете системните настройки"), + ("How to get Android input permission?", "Как да получим право за въвеждане под Андрид?"), + ("android_input_permission_tip1", "За да може отдалечено устройство да управлява вашето Android устройство чрез мишка или допир, трябва да разрешите на RustDesk да използва услугата \"Достъпност\"."), ("android_input_permission_tip2", "Моля, отидете на следващата страница с системни настройки, намерете и въведете [Installed Services], включете услугата [RustDesk Input]."), - ("android_new_connection_tip", "Получена е нова заявка за контрол, която иска да контролира вашето текущо устройство."), - ("android_service_will_start_tip", "Включването на \"Заснемане на екрана\" автоматично ще стартира услугата, позволявайки на други устройства да поискат връзка с вашето устройство."), + ("android_new_connection_tip", "Получена е нова заявка за отдалечено управление на вашето текущо устройство."), + ("android_service_will_start_tip", "Включването на \"Снемане на екрана\" автоматично ще стартира услугата, позволявайки на други устройства да поискат връзка с вашето устройство."), ("android_stop_service_tip", "Затварянето на услугата автоматично ще затвори всички установени връзки."), - ("android_version_audio_tip", "Текущата версия на Android не поддържа аудио заснемане, моля, актуализирайте устройството с Android 10 или по-нова версия."), - ("android_start_service_tip", "Докоснете [Start service] или активирайте разрешение [Screen Capture], за да стартирате услугата за споделяне на екрана."), - ("android_permission_may_not_change_tip", "Разрешенията за установени връзки може да не се променят незабавно, докато не се свържете отново."), - ("Account", "Акаунт"), + ("android_version_audio_tip", "Текущата версия на Android не поддържа аудиозапис. Моля, актуализирайте устройството с Android 10 или по-нов."), + ("android_start_service_tip", "Докоснете [Start service] или позволете [Screen Capture], за да започне услугата по споделяне на екрана."), + ("android_permission_may_not_change_tip", "Разрешенията за установени връзки може да не се променят незабавно, а ще изискват да се свържете отново."), + ("Account", "Сметка"), ("Overwrite", "Презаписване"), - ("This file exists, skip or overwrite this file?", ""), - ("Quit", "Излез"), + ("This file exists, skip or overwrite this file?", "Този файл съществува вече. Пропускане или презаписване?"), + ("Quit", "Изход"), ("Help", "Помощ"), ("Failed", "Неуспешно"), ("Succeeded", "Успешно"), - ("Someone turns on privacy mode, exit", "Някой включва режим на поверителност, излезте"), - ("Unsupported", "Не се поддържа"), - ("Peer denied", ""), - ("Please install plugins", ""), - ("Peer exit", ""), - ("Failed to turn off", ""), - ("Turned off", ""), + ("Someone turns on privacy mode, exit", "Някой включва режим на поверителност, изход"), + ("Unsupported", "Неподдържан"), + ("Peer denied", "Отказ от другата страна"), + ("Please install plugins", "Моля поставете приставки"), + ("Peer exit", "Изход от другата страна"), + ("Failed to turn off", "Провал при опит за изключване"), + ("Turned off", "Изкключен"), ("Language", "Език"), - ("Keep RustDesk background service", ""), + ("Keep RustDesk background service", "Запази работеща фонова услуга с RustDesk"), ("Ignore Battery Optimizations", "Игнорирай оптимизациите на батерията"), ("android_open_battery_optimizations_tip", "Ако искате да деактивирате тази функция, моля, отидете на следващата страница с настройки на приложението RustDesk, намерете и въведете [Battery], премахнете отметката от [Unrestricted]"), ("Start on boot", "Стартирайте при зареждане"), ("Start the screen sharing service on boot, requires special permissions", ""), - ("Connection not allowed", ""), - ("Legacy mode", ""), - ("Map mode", ""), - ("Translate mode", "Режим на превод"), - ("Use permanent password", "Използвайте постоянна парола"), - ("Use both passwords", "Използвайте и двете пароли"), - ("Set permanent password", "Задайте постоянна парола"), - ("Enable remote restart", "Разрешете отдалечено рестартиране"), - ("Restart remote device", "Рестартирайте отдалеченото устройство"), + ("Connection not allowed", "Връзката непозволена"), + ("Legacy mode", "По остарял начин"), + ("Map mode", "По начин със съответствие (map)"), + ("Translate mode", "По нчаин с превод"), + ("Use permanent password", "Използване на постоянна парола"), + ("Use both passwords", "Използване и на двете пароли"), + ("Set permanent password", "Задаване постоянна парола"), + ("Enable remote restart", "Разрешаване на отдалечен рестарт"), + ("Restart remote device", "Рестартиране на отдалечено устройство"), ("Are you sure you want to restart", "Сигурни ли сте, че искате да рестартирате"), - ("Restarting remote device", "Рестартира се отдалечено устройство"), + ("Restarting remote device", "Рестартиране на отдалечено устройство"), ("remote_restarting_tip", "Отдалеченото устройство се рестартира, моля, затворете това съобщение и се свържете отново с постоянна парола след известно време"), - ("Copied", "Копирано"), + ("Copied", "Преписано"), ("Exit Fullscreen", "Изход от цял екран"), ("Fullscreen", "Цял екран"), ("Mobile Actions", "Мобилни действия"), @@ -334,10 +334,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Hide Toolbar", "Скриване на лентата с инструменти"), ("Direct Connection", "Директна връзка"), ("Relay Connection", "Релейна връзка"), - ("Secure Connection", "Защитена връзка"), - ("Insecure Connection", "Незащитена връзка"), + ("Secure Connection", "Сигурна връзка"), + ("Insecure Connection", "Несигурна връзка"), ("Scale original", "Оригинален мащаб"), - ("Scale adaptive", "Адаптивно мащабиране"), + ("Scale adaptive", "Приспособимо мащабиране"), ("General", "Основен"), ("Security", "Сигурност"), ("Theme", "Тема"), @@ -345,128 +345,128 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Light Theme", "Светла тема"), ("Dark", "Тъмна"), ("Light", "Светла"), - ("Follow System", "Следвай системата"), - ("Enable hardware codec", "Активиране на хардуерен кодек"), + ("Follow System", "Следвай система"), + ("Enable hardware codec", "Позволяване хардуерен кодек"), ("Unlock Security Settings", "Отключи настройките за сигурност"), ("Enable audio", "Разрешете аудиото"), ("Unlock Network Settings", "Отключи мрежовите настройки"), ("Server", "Сървър"), - ("Direct IP Access", "Директен IP достъп"), - ("Proxy", "Прокси"), - ("Apply", "Приложи"), - ("Disconnect all devices?", ""), - ("Clear", "Изчисти"), + ("Direct IP Access", "Пряк IP достъп"), + ("Proxy", "Посредник (Proxy)"), + ("Apply", "Прилагане"), + ("Disconnect all devices?", "Разкачване на всички устройства"), + ("Clear", "Изчистване"), ("Audio Input Device", "Аудио входно устройство"), - ("Use IP Whitelisting", "Използвайте бял списък с IP адреси"), + ("Use IP Whitelisting", "Използване бял списък с IP адреси"), ("Network", "Мрежа"), - ("Pin Toolbar", "Фиксиране на лентата с инструменти"), - ("Unpin Toolbar", "Откачване на лентата с инструменти"), + ("Pin Toolbar", "Закачане лента с инструменти"), + ("Unpin Toolbar", "Откачюане лента с инструменти"), ("Recording", "Записване"), ("Directory", "Директория"), - ("Automatically record incoming sessions", ""), - ("Change", "Промени"), - ("Start session recording", ""), - ("Stop session recording", ""), - ("Enable recording session", ""), - ("Enable LAN discovery", "Активирайте откриване в LAN"), - ("Deny LAN discovery", "Забранете откриване в LAN"), + ("Automatically record incoming sessions", "Автоматичен запис на входящи сесии"), + ("Change", "Промяна"), + ("Start session recording", "Започванена запис"), + ("Stop session recording", "Край на запис"), + ("Enable recording session", "Позволяване запис"), + ("Enable LAN discovery", "Позволяване откриване във вътрешна мрежа"), + ("Deny LAN discovery", "Забрана за откриване във вътрешна мрежа"), ("Write a message", "Напишете съобщение"), ("Prompt", "Подкана"), - ("Please wait for confirmation of UAC...", ""), + ("Please wait for confirmation of UAC...", "Моля изчакайте за потвърждение от UAC..."), ("elevated_foreground_window_tip", "Текущият прозорец на отдалечения работен плот изисква по-високи привилегии за работа, така че временно не може да използва мишката и клавиатурата. Можете да поискате от отдалечения потребител да минимизира текущия прозорец или да щракнете върху бутона за повдигане в прозореца за управление на връзката. За да избегнете този проблем, се препоръчва да инсталирате софтуера на отдалеченото устройство."), ("Disconnected", "Прекъсната връзка"), ("Other", "Други"), - ("Confirm before closing multiple tabs", ""), + ("Confirm before closing multiple tabs", "Потвърждение преди затваряне на няколко раздела"), ("Keyboard Settings", "Настройки на клавиатурата"), ("Full Access", "Пълен достъп"), ("Screen Share", "Споделяне на екрана"), - ("Wayland requires Ubuntu 21.04 or higher version.", ""), - ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", ""), - ("JumpLink", "Преглед"), - ("Please Select the screen to be shared(Operate on the peer side).", "Моля, изберете екрана, който да бъде споделен (Работете от страна на партньора)."), + ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland изисква Ubuntu 21.04 или по-нов"), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland изисква по-нов Linux. Моля, опитайте с X11 или сменете операционната система."), + ("JumpLink", "Препратка"), + ("Please Select the screen to be shared(Operate on the peer side).", "Моля, изберете екрана, който да бъде споделен (спрямо отдалечената страна)."), ("Show RustDesk", "Покажи RustDesk"), ("This PC", "Този компютър"), ("or", "или"), ("Continue with", "Продължи с"), ("Elevate", "Повишаване"), - ("Zoom cursor", "Мащабиране на Курсор"), - ("Accept sessions via password", "Приемайте сесии чрез парола"), - ("Accept sessions via click", "Приемане на сесии чрез щракване"), - ("Accept sessions via both", "Приемайте сесии и през двете"), - ("Please wait for the remote side to accept your session request...", ""), + ("Zoom cursor", "Уголемяване курсор"), + ("Accept sessions via password", "Приемане сесии чрез парола"), + ("Accept sessions via click", "Приемане сесии чрез цъкване"), + ("Accept sessions via both", "Приемане сесии и по двата начина"), + ("Please wait for the remote side to accept your session request...", "Моля, изчакайте докато другата страна приеме заявката за отдалечен достъп..."), ("One-time Password", "Еднократна парола"), - ("Use one-time password", ""), - ("One-time password length", ""), - ("Request access to your device", ""), - ("Hide connection management window", ""), - ("hide_cm_tip", "Разрешете скриването само ако приемате сесии чрез парола и използвате постоянна парола"), - ("wayland_experiment_tip", "Wayland support is in experimental stage, please use X11 if you require unattended access."), - ("Right click to select tabs", ""), + ("Use one-time password", "Ползване на еднократна парола"), + ("One-time password length", "Дължина на еднократна парола"), + ("Request access to your device", "Искане за достъп до ваше устройство"), + ("Hide connection management window", "Скриване на прозореца за управление на свързване"), + ("hide_cm_tip", "Разрешаване скриване само ако се приемат сесии чрез постоянна парола"), + ("wayland_experiment_tip", "Поддръжката на Wayland е в експериментален стадий, моля, използвайте X11, ако се нуждаете от безконтролен достъп.."), + ("Right click to select tabs", "Десен бутон за избор на раздел"), ("Skipped", "Пропуснато"), - ("Add to address book", ""), + ("Add to address book", "Добавяне към познати адреси"), ("Group", "Група"), ("Search", "Търсене"), - ("Closed manually by web console", ""), - ("Local keyboard type", ""), - ("Select local keyboard type", ""), + ("Closed manually by web console", "Затворен ръчно от уеб конзола"), + ("Local keyboard type", "Тип на тукашната клавиатура"), + ("Select local keyboard type", "Избор на тип на тукашната клавиатура"), ("software_render_tip", "Ако използвате графична карта Nvidia под Linux и отдалеченият прозорец се затваря веднага след свързване, превключването към драйвера Nouveau с отворен код и изборът да използвате софтуерно изобразяване може да помогне. Изисква се рестартиране на софтуера."), - ("Always use software rendering", ""), - ("config_input", "За да контролирате отдалечен работен плот с клавиатура, трябва да предоставите на RustDesk разрешения \"Input Monitoring\"."), - ("config_microphone", "За да говорите дистанционно, трябва да предоставите на RustDesk разрешения \"Запис на звук\"."), - ("request_elevation_tip", "Можете също така да поискате повишаване на привилегии, ако има някой от отдалечената страна."), - ("Wait", "Изчакайте"), - ("Elevation Error", "Грешка при повишаване на привилегии"), - ("Ask the remote user for authentication", ""), - ("Choose this if the remote account is administrator", ""), - ("Transmit the username and password of administrator", ""), - ("still_click_uac_tip", "Все още изисква отдалеченият потребител да щракне върху OK в прозореца на UAC при стартиранят RustDesk."), - ("Request Elevation", "Поискайте повишаване на привилегии"), + ("Always use software rendering", "Винаги ползвай софтуерно изграждане на картината"), + ("config_input", "За да управлявате отдалечена среда с клавиатура, трябва да предоставите на RustDesk право за \"Input Monitoring\"."), + ("config_microphone", "За да говорите отдалечено, трябва да предоставите на RustDesk право за \"Запис на звук\"."), + ("request_elevation_tip", "Можете също така да поискате разширени права, ако има някой от отдалечената страна."), + ("Wait", "Изчакване"), + ("Elevation Error", "Грешка при добвиане на разширени права"), + ("Ask the remote user for authentication", "Попитайте отдалечения потребител за удостоверяване"), + ("Choose this if the remote account is administrator", "Изберете това, ако отдалеченият потребител е администратор."), + ("Transmit the username and password of administrator", "Предаване на потребителското име и паролата на администратора"), + ("still_click_uac_tip", "Все още изисква отдалеченият потребител да щракне върху OK в прозореца на UAC при стартиран RustDesk."), + ("Request Elevation", "Поискайте разширени права"), ("wait_accept_uac_tip", "Моля, изчакайте отдалеченият потребител да приеме диалоговия прозорец на UAC."), - ("Elevate successfully", ""), - ("uppercase", ""), - ("lowercase", ""), - ("digit", ""), - ("special character", ""), - ("length>=8", ""), - ("Weak", ""), - ("Medium", ""), - ("Strong", ""), - ("Switch Sides", "Сменете страните"), - ("Please confirm if you want to share your desktop?", ""), - ("Display", ""), + ("Elevate successfully", "Успешно получаване на разширени права"), + ("uppercase", "големи букви"), + ("lowercase", "малки букви"), + ("digit", "цифра"), + ("special character", "специален знак"), + ("length>=8", "дължина>=8"), + ("Weak", "Слаба"), + ("Medium", "Средна"), + ("Strong", "Силна"), + ("Switch Sides", "Размяна на страните"), + ("Please confirm if you want to share your desktop?", "Моля, потвърдете дали искате да споделите работното си пространство"), + ("Display", "Екран"), ("Default View Style", "Стил на изглед по подразбиране"), ("Default Scroll Style", "Стил на превъртане по подразбиране"), ("Default Image Quality", "Качество на изображението по подразбиране"), ("Default Codec", "Кодек по подразбиране"), - ("Bitrate", "Битрейт"), + ("Bitrate", "Скорост на предаване на данни (bitrate)"), ("FPS", "Кадри в секунда"), ("Auto", "Автоматично"), ("Other Default Options", "Други опции по подразбиране"), - ("Voice call", ""), - ("Text chat", ""), - ("Stop voice call", ""), + ("Voice call", "Гласови обаждания"), + ("Text chat", "Текстов разговор"), + ("Stop voice call", "Прекратяване гласово обаждане"), ("relay_hint_tip", "Може да не е възможно да се свържете директно; можете да опитате да се свържете чрез реле. Освен това, ако искате да използвате реле при първия си опит, добавете наставка \"/r\" към идентификатора или да изберете опцията \"Винаги свързване чрез реле\" в картата на последните сесии, ако съществува."), - ("Reconnect", "Свържете се отново"), + ("Reconnect", "Повторно свързане"), ("Codec", "Кодек"), - ("Resolution", "Резолюция"), - ("No transfers in progress", "Не се извършват трансфери"), - ("Set one-time password length", ""), + ("Resolution", "Разделителна способност"), + ("No transfers in progress", "Няма текущи прехвърляния"), + ("Set one-time password length", "Задаване дължаина на еднократна парола"), ("RDP Settings", "RDP настройки"), - ("Sort by", "Сортирай по"), + ("Sort by", "Подредба по"), ("New Connection", "Ново свързване"), - ("Restore", ""), - ("Minimize", ""), - ("Maximize", ""), + ("Restore", "Възстановяване"), + ("Minimize", "Смаляване"), + ("Maximize", "Уголемяване"), ("Your Device", "Вашето устройство"), ("empty_recent_tip", "Ами сега, няма скорошни сесии!\nВреме е да планирате нова."), ("empty_favorite_tip", "Все още нямате любими връстници?\nНека намерим някой, с когото да се свържете, и да го добавим към вашите любими!"), ("empty_lan_tip", "О, не, изглежда, че все още не сме открили връстници."), ("empty_address_book_tip", "Изглежда, че в момента няма изброени връстници във вашата адресна книга."), - ("eg: admin", ""), + ("eg: admin", "напр. admin"), ("Empty Username", "Празно потребителско име"), ("Empty Password", "Празна парола"), - ("Me", "Аз"), - ("identical_file_tip", "Този файл е идентичен с този на партньора."), + ("Me", "Мен"), + ("identical_file_tip", "Файлът съвпада с този от другата страна."), ("show_monitors_tip", "Показване на мониторите в лентата с инструменти"), ("View Mode", "Режим на преглед"), ("login_linux_tip", "Трябва да влезете в отдалечен Linux акаунт, за да активирате X сесия на работния плот"), @@ -482,47 +482,47 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("no_desktop_text_tip", "Моля, инсталирайте работен плот GNOME"), ("No need to elevate", ""), ("System Sound", "Системен звук"), - ("Default", ""), - ("New RDP", ""), - ("Fingerprint", ""), + ("Default", "По подразбиране"), + ("New RDP", "Нов RDP"), + ("Fingerprint", "Пръстов отпечатък"), ("Copy Fingerprint", "Копиране на пръстов отпечатък"), ("no fingerprints", "Няма пръстови отпечатъци"), - ("Select a peer", ""), - ("Select peers", ""), - ("Plugins", ""), - ("Uninstall", ""), - ("Update", ""), - ("Enable", ""), - ("Disable", ""), + ("Select a peer", "Избери отдалечена страна"), + ("Select peers", "Избери отдалечени страни"), + ("Plugins", "Приставки"), + ("Uninstall", "Премахни"), + ("Update", "Обновяване"), + ("Enable", "Позволяване"), + ("Disable", "Забрана"), ("Options", "Настроики"), - ("resolution_original_tip", "Оригинална резолюция"), - ("resolution_fit_local_tip", "Напасване към локална разделителна способност"), - ("resolution_custom_tip", "Персонализирана разделителна способност"), + ("resolution_original_tip", "Оригинална разделителна способност"), + ("resolution_fit_local_tip", "Приспособяване към тукашната разделителна способност"), + ("resolution_custom_tip", "Разделителна способност по свой избор"), ("Collapse toolbar", "Свиване на лентата с инструменти"), - ("Accept and Elevate", "Приемете и повишаване на привилегии"), - ("accept_and_elevate_btn_tooltip", "Приемете връзката и повишете UAC разрешенията."), - ("clipboard_wait_response_timeout_tip", "Времето за изчакване на отговор за копиране изтече."), - ("Incoming connection", ""), - ("Outgoing connection", ""), - ("Exit", "Излез"), - ("Open", "Отвори"), + ("Accept and Elevate", "Приемане и предоставяне на допълнителни права"), + ("accept_and_elevate_btn_tooltip", "Приемане на връзката предоставяне на UAC разрешения."), + ("clipboard_wait_response_timeout_tip", "Времето за изчакване на отговор за препис изтече."), + ("Incoming connection", "Входяща връзка"), + ("Outgoing connection", "Изходяща връзка"), + ("Exit", "Изход"), + ("Open", "Отваряне"), ("logout_tip", "Сигурни ли сте, че искате да излезете?"), ("Service", "Услуга"), ("Start", "Стартиране"), ("Stop", "Спиране"), ("exceed_max_devices", "Достигнахте максималния брой управлявани устройства."), - ("Sync with recent sessions", ""), - ("Sort tags", ""), - ("Open connection in new tab", ""), - ("Move tab to new window", ""), - ("Can not be empty", ""), - ("Already exists", ""), - ("Change Password", "Промяна на паролата"), - ("Refresh Password", "Обнови паролата"), - ("ID", ""), + ("Sync with recent sessions", "Синхронизиране с последните сесии"), + ("Sort tags", "Подреди белези"), + ("Open connection in new tab", "Разкриване на връзка в нов раздел"), + ("Move tab to new window", "Отделяне на раздела в нов прозорец"), + ("Can not be empty", "Не може да е празно"), + ("Already exists", "Вече съществува"), + ("Change Password", "Промяна на парола"), + ("Refresh Password", "Обновяване парола"), + ("ID", "Определител (ID)"), ("Grid View", "Мрежов изглед"), ("List View", "Списъчен изглед"), - ("Select", ""), + ("Select", "Избиране"), ("Toggle Tags", "Превключване на етикети"), ("pull_ab_failed_tip", "Неуспешно опресняване на адресната книга"), ("push_ab_failed_tip", "Неуспешно синхронизиране на адресната книга със сървъра"), @@ -530,119 +530,119 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change Color", "Промяна на цвета"), ("Primary Color", "Основен цвят"), ("HSV Color", "HSV цвят"), - ("Installation Successful!", "Успешна инсталация!"), - ("Installation failed!", ""), - ("Reverse mouse wheel", ""), - ("{} sessions", ""), + ("Installation Successful!", "Успешно поставяне!"), + ("Installation failed!", "Провал при поставяне"), + ("Reverse mouse wheel", "Обърнато колелото на мишката"), + ("{} sessions", "{} сесии"), ("scam_title", "Възможно е да сте ИЗМАМЕНИ!"), ("scam_text1", "Ако разговаряте по телефона с някой, когото НЕ ПОЗНАВАТЕ и НЯМАТЕ ДОВЕРИЕ, който ви е помолил да използвате RustDesk и да стартирате услугата, не продължавайте и затворете незабавно."), ("scam_text2", "Те вероятно са измамник, който се опитва да открадне вашите пари или друга лична информация."), ("Don't show again", "Не показвай отново"), - ("I Agree", ""), - ("Decline", ""), - ("Timeout in minutes", ""), + ("I Agree", "Съгласен"), + ("Decline", "Отказвам"), + ("Timeout in minutes", "Време за отговор в минути"), ("auto_disconnect_option_tip", "Автоматично затваряне на входящите сесии при неактивност на потребителя"), ("Connection failed due to inactivity", "Автоматично прекъсване на връзката поради неактивност"), ("Check for software update on startup", ""), ("upgrade_rustdesk_server_pro_to_{}_tip", "Моля обновете RustDesk Server Pro на версия {} или по-нова!"), ("pull_group_failed_tip", "Неуспешно опресняване на групата"), - ("Filter by intersection", ""), - ("Remove wallpaper during incoming sessions", ""), - ("Test", ""), + ("Filter by intersection", "Отсяване по пресичане"), + ("Remove wallpaper during incoming sessions", "Спри фоновото изображение по време на входящи сесии"), + ("Test", "Проверка"), ("display_is_plugged_out_msg", "Дисплеят е изключен, превключете на първия монитор."), - ("No displays", ""), - ("Open in new window", ""), - ("Show displays as individual windows", ""), - ("Use all my displays for the remote session", ""), + ("No displays", "Няма екрани"), + ("Open in new window", "Отваряне в нов прозорец"), + ("Show displays as individual windows", "Показване на екраните в отделни прозорци"), + ("Use all my displays for the remote session", "Използване на всички тукашни екрани за отдалечена работа"), ("selinux_tip", "SELinux е активиран на вашето устройство, което може да попречи на RustDesk да работи правилно като контролирана страна."), - ("Change view", ""), - ("Big tiles", ""), - ("Small tiles", ""), - ("List", ""), - ("Virtual display", ""), - ("Plug out all", ""), + ("Change view", "Промяна изглед"), + ("Big tiles", "Големи заглавия"), + ("Small tiles", "Малки заглавия"), + ("List", "Списък"), + ("Virtual display", "Виртуален екран"), + ("Plug out all", "Изтръгване на всички"), ("True color (4:4:4)", ""), ("Enable blocking user input", "Разрешаване на блокиране на потребителско въвеждане"), ("id_input_tip", "Можете да въведете ID, директен IP адрес или домейн с порт (:).\nАко искате да получите достъп до устройство на друг сървър, моля, добавете адреса на сървъра (@?key=), например\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nАко искате да получите достъп до устройство на обществен сървър, моля, въведете \"@public\" , ключът не е необходим за публичен сървър"), ("privacy_mode_impl_mag_tip", "Режим 1"), ("privacy_mode_impl_virtual_display_tip", "Режим 2"), - ("Enter privacy mode", ""), - ("Exit privacy mode", ""), + ("Enter privacy mode", "Влизане в поверителен режим"), + ("Exit privacy mode", "Изход от поверителен режим"), ("idd_not_support_under_win10_2004_tip", "Индиректен драйвер за дисплей не се поддържа. Изисква се Windows 10, версия 2004 или по-нова."), ("input_source_1_tip", "Входен източник 1"), ("input_source_2_tip", "Входен източник 2"), ("Swap control-command key", ""), - ("swap-left-right-mouse", "Разменете левия и десния бутон на мишката"), - ("2FA code", "Код за Двуфакторна удостоверяване"), + ("swap-left-right-mouse", "Размяна на копчетата на мишката"), + ("2FA code", "Код за Двуфакторно удостоверяване"), ("More", "Повече"), - ("enable-2fa-title", "Активиране на двуфакторно удостоверяване"), + ("enable-2fa-title", "Позволяване на двуфакторно удостоверяване"), ("enable-2fa-desc", "Моля, настройте вашия удостоверител сега. Можете да използвате приложение за удостоверяване като Authy, Microsoft или Google Authenticator на вашия телефон или настолен компютър.\n\nСканирайте QR кода с вашето приложение и въведете кода, който приложението ви показва, за да активирате двуфакторно удостоверяване."), ("wrong-2fa-code", "е може да се потвърди кодът. Проверете дали настройките за код и локалното време са правилни"), ("enter-2fa-title", "Двуфакторно удостоверяване"), - ("Email verification code must be 6 characters.", ""), - ("2FA code must be 6 digits.", ""), - ("Multiple Windows sessions found", ""), - ("Please select the session you want to connect to", ""), + ("Email verification code must be 6 characters.", "Кодът за проверка следва да е с дължина 6 знака."), + ("2FA code must be 6 digits.", "Кодът за 2FA (двуфакторно удостоверяване) трябва да е 6-цифрен"), + ("Multiple Windows sessions found", "Установени са няколко Windwos сесии"), + ("Please select the session you want to connect to", "Моля определете сесия към която искате да се свърженете"), ("powered_by_me", ""), ("outgoing_only_desk_tip", ""), ("preset_password_warning", ""), - ("Security Alert", ""), - ("My address book", ""), - ("Personal", ""), - ("Owner", ""), - ("Set shared password", ""), - ("Exist in", ""), - ("Read-only", ""), - ("Read/Write", ""), - ("Full Control", ""), + ("Security Alert", "Предупреждение за сигурност"), + ("My address book", "Моята адресна книга"), + ("Personal", "Личен"), + ("Owner", "Собственик"), + ("Set shared password", "Определяне споделена парола"), + ("Exist in", "Съществува в"), + ("Read-only", "Само четене"), + ("Read/Write", "Писане/четене"), + ("Full Control", "Пълен контрол"), ("share_warning_tip", ""), - ("Everyone", ""), + ("Everyone", "Всички"), ("ab_web_console_tip", ""), ("allow-only-conn-window-open-tip", ""), ("no_need_privacy_mode_no_physical_displays_tip", ""), - ("Follow remote cursor", ""), - ("Follow remote window focus", ""), + ("Follow remote cursor", "Следвай отдалечения курсор"), + ("Follow remote window focus", "Следвай фокуса на отдалечените прозорци"), ("default_proxy_tip", ""), ("no_audio_input_device_tip", ""), - ("Incoming", ""), - ("Outgoing", ""), - ("Clear Wayland screen selection", ""), + ("Incoming", "Входящ"), + ("Outgoing", "Изходящ"), + ("Clear Wayland screen selection", "Изчистване избор на Wayland екран"), ("clear_Wayland_screen_selection_tip", ""), ("confirm_clear_Wayland_screen_selection_tip", ""), ("android_new_voice_call_tip", ""), ("texture_render_tip", ""), - ("Use texture rendering", ""), - ("Floating window", ""), + ("Use texture rendering", "Използвай текстово изграждане"), + ("Floating window", "Плаващ прозорец"), ("floating_window_tip", ""), - ("Keep screen on", ""), - ("Never", ""), - ("During controlled", ""), - ("During service is on", ""), - ("Capture screen using DirectX", ""), + ("Keep screen on", "Запази екранът включен"), + ("Never", "Никога"), + ("During controlled", "Докато е обект на управление"), + ("During service is on", "Докато услугата е включена"), + ("Capture screen using DirectX", "Снемай екрана ползвайки DirectX"), ("Back", "Назад"), - ("Apps", ""), - ("Volume up", ""), - ("Volume down", ""), - ("Power", ""), - ("Telegram bot", ""), + ("Apps", "Приложения"), + ("Volume up", "Усилване звук"), + ("Volume down", "Намаляне звук"), + ("Power", "Мощност"), + ("Telegram bot", "Телеграм бот"), ("enable-bot-tip", ""), ("enable-bot-desc", ""), ("cancel-2fa-confirm-tip", ""), ("cancel-bot-confirm-tip", ""), - ("About RustDesk", ""), + ("About RustDesk", "Относно RustDesk"), ("Send clipboard keystrokes", ""), ("network_error_tip", ""), - ("Unlock with PIN", ""), + ("Unlock with PIN", "Отключване с PIN"), ("Requires at least {} characters", ""), - ("Wrong PIN", ""), - ("Set PIN", ""), - ("Enable trusted devices", ""), - ("Manage trusted devices", ""), - ("Platform", ""), - ("Days remaining", ""), + ("Wrong PIN", "Грешен PIN"), + ("Set PIN", "Избор PIN"), + ("Enable trusted devices", "Позволяване доверени устройства"), + ("Manage trusted devices", "Управление доверени устройства"), + ("Platform", "Платформа"), + ("Days remaining", "Оставащи дни"), ("enable-trusted-devices-tip", ""), ("Parent directory", ""), - ("Resume", ""), - ("Invalid file name", ""), + ("Resume", "Възобновяване"), + ("Invalid file name", "Невалидно име за файл"), ].iter().cloned().collect(); } From 9380f33d7c08b3c3db8659df5915feda22ab192a Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Tue, 10 Sep 2024 23:54:59 +0800 Subject: [PATCH 179/541] Refact/options (#9318) * refact options Signed-off-by: fufesou * Remove unused msg Signed-off-by: fufesou * web, toggle virtual display Signed-off-by: fufesou --------- Signed-off-by: fufesou --- .../lib/desktop/widgets/remote_toolbar.dart | 4 +++- flutter/lib/models/model.dart | 2 +- flutter/lib/web/bridge.dart | 10 +++++---- libs/scrap/src/common/mod.rs | 2 +- src/client.rs | 22 ------------------- src/client/io_loop.rs | 17 -------------- src/flutter.rs | 6 ++--- 7 files changed, 14 insertions(+), 49 deletions(-) diff --git a/flutter/lib/desktop/widgets/remote_toolbar.dart b/flutter/lib/desktop/widgets/remote_toolbar.dart index f881bc84007c..2d20d5931c9e 100644 --- a/flutter/lib/desktop/widgets/remote_toolbar.dart +++ b/flutter/lib/desktop/widgets/remote_toolbar.dart @@ -1718,7 +1718,9 @@ class _KeyboardMenu extends StatelessWidget { if (value == null) return; await bind.sessionToggleOption( sessionId: ffi.sessionId, value: kOptionToggleViewOnly); - ffiModel.setViewOnly(id, value); + final viewOnly = await bind.sessionGetToggleOption( + sessionId: ffi.sessionId, arg: kOptionToggleViewOnly); + ffiModel.setViewOnly(id, viewOnly ?? value); } : null, ffi: ffi, diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 853cf8ac073e..084ac1b43a68 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -795,7 +795,7 @@ class FfiModel with ChangeNotifier { isRefreshing = false; } Map features = json.decode(evt['features']); - _pi.features.privacyMode = features['privacy_mode'] == 1; + _pi.features.privacyMode = features['privacy_mode'] == true; if (!isCache) { handleResolutions(peerId, evt["resolutions"]); } diff --git a/flutter/lib/web/bridge.dart b/flutter/lib/web/bridge.dart index 97cac31c9347..26e94ff1c589 100644 --- a/flutter/lib/web/bridge.dart +++ b/flutter/lib/web/bridge.dart @@ -196,8 +196,8 @@ class RustdeskImpl { required bool on, dynamic hint}) { return Future(() => js.context.callMethod('setByName', [ - 'option:toggle', - jsonEncode({implKey, on}) + 'toggle_privacy_mode', + jsonEncode({'impl_key': implKey, 'on': on}) ])); } @@ -1204,8 +1204,10 @@ class RustdeskImpl { required int index, required bool on, dynamic hint}) { - // TODO - throw UnimplementedError(); + return Future(() => js.context.callMethod('setByName', [ + 'toggle_virtual_display', + jsonEncode({'index': index, 'on': on}) + ])); } Future mainSetHomeDir({required String home, dynamic hint}) { diff --git a/libs/scrap/src/common/mod.rs b/libs/scrap/src/common/mod.rs index 164f7157de30..d6060e1315f8 100644 --- a/libs/scrap/src/common/mod.rs +++ b/libs/scrap/src/common/mod.rs @@ -316,7 +316,7 @@ impl ToString for CodecFormat { CodecFormat::AV1 => "AV1".into(), CodecFormat::H264 => "H264".into(), CodecFormat::H265 => "H265".into(), - CodecFormat::Unknown => "Unknow".into(), + CodecFormat::Unknown => "Unknown".into(), } } } diff --git a/src/client.rs b/src/client.rs index f1df8d20e48f..62a3ca0a870e 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1800,28 +1800,6 @@ impl LoginConfigHandler { ) } - pub fn get_option_message_after_login(&self) -> Option { - if self.conn_type.eq(&ConnType::FILE_TRANSFER) - || self.conn_type.eq(&ConnType::PORT_FORWARD) - || self.conn_type.eq(&ConnType::RDP) - { - return None; - } - let mut n = 0; - let mut msg = OptionMessage::new(); - if self.version < hbb_common::get_version_number("1.2.4") { - if self.get_toggle_option("privacy-mode") { - msg.privacy_mode = BoolOption::Yes.into(); - n += 1; - } - } - if n > 0 { - Some(msg) - } else { - None - } - } - /// Parse the image quality option. /// Return [`ImageQuality`] if the option is valid, otherwise return `None`. /// diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index 1a209ca0a533..21a5af9a3599 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -958,22 +958,6 @@ impl Remote { true } - async fn send_opts_after_login(&self, peer: &mut Stream) { - if let Some(opts) = self - .handler - .lc - .read() - .unwrap() - .get_option_message_after_login() - { - let mut misc = Misc::new(); - misc.set_option(opts); - let mut msg_out = Message::new(); - msg_out.set_misc(misc); - allow_err!(peer.send(&msg_out).await); - } - } - async fn send_toggle_virtual_display_msg(&self, peer: &mut Stream) { if !self.peer_info.is_support_virtual_display() { return; @@ -1135,7 +1119,6 @@ impl Remote { self.first_frame = true; self.handler.close_success(); self.handler.adapt_size(); - self.send_opts_after_login(peer).await; self.send_toggle_virtual_display_msg(peer).await; self.send_toggle_privacy_mode_msg(peer).await; } diff --git a/src/flutter.rs b/src/flutter.rs index 03b5a5750309..717615753b8a 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -802,13 +802,13 @@ impl InvokeUiSession for FlutterHandler { fn set_peer_info(&self, pi: &PeerInfo) { let displays = Self::make_displays_msg(&pi.displays); - let mut features: HashMap<&str, i32> = Default::default(); + let mut features: HashMap<&str, bool> = Default::default(); for ref f in pi.features.iter() { - features.insert("privacy_mode", if f.privacy_mode { 1 } else { 0 }); + features.insert("privacy_mode", f.privacy_mode); } // compatible with 1.1.9 if get_version_number(&pi.version) < get_version_number("1.2.0") { - features.insert("privacy_mode", 0); + features.insert("privacy_mode", false); } let features = serde_json::ser::to_string(&features).unwrap_or("".to_owned()); let resolutions = serialize_resolutions(&pi.resolutions.resolutions); From cbca0eb34070d63c0e89f467e69c59c6a791f5fd Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Wed, 11 Sep 2024 10:01:03 +0800 Subject: [PATCH 180/541] fix: keyboard, move tab to new window (#9322) Do not disable keyboard when moving tab to new window on dispose. Signed-off-by: fufesou --- flutter/lib/desktop/pages/remote_page.dart | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 341025c5f3c9..4ef8157da19b 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -245,8 +245,10 @@ class _RemotePageState extends State super.dispose(); debugPrint("REMOTE PAGE dispose session $sessionId ${widget.id}"); _ffi.textureModel.onRemotePageDispose(closeSession); - // ensure we leave this session, this is a double check - _ffi.inputModel.enterOrLeave(false); + if (closeSession) { + // ensure we leave this session, this is a double check + _ffi.inputModel.enterOrLeave(false); + } DesktopMultiWindow.removeListener(this); _ffi.dialogManager.hideMobileActionsOverlay(); _ffi.imageModel.disposeImage(); From 2e81bcb4475c1143c52a9561d1d292f464fb49b2 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Wed, 11 Sep 2024 17:17:32 +0800 Subject: [PATCH 181/541] fix: Query online, remove loop retry (#9326) Signed-off-by: fufesou --- src/client.rs | 72 +++++++++++++++++++------------------------------- src/flutter.rs | 35 ++++++++++++------------ 2 files changed, 44 insertions(+), 63 deletions(-) diff --git a/src/client.rs b/src/client.rs index 62a3ca0a870e..9e49b84e2e27 100644 --- a/src/client.rs +++ b/src/client.rs @@ -3414,7 +3414,6 @@ pub mod peer_online { tcp::FramedStream, ResultType, }; - use std::time::Instant; pub async fn query_online_states, Vec)>(ids: Vec, f: F) { let test = false; @@ -3424,29 +3423,14 @@ pub mod peer_online { let offlines = onlines.drain((onlines.len() / 2)..).collect(); f(onlines, offlines) } else { - let query_begin = Instant::now(); let query_timeout = std::time::Duration::from_millis(3_000); - loop { - match query_online_states_(&ids, query_timeout).await { - Ok((onlines, offlines)) => { - f(onlines, offlines); - break; - } - Err(e) => { - log::debug!("{}", &e); - } + match query_online_states_(&ids, query_timeout).await { + Ok((onlines, offlines)) => { + f(onlines, offlines); } - - if query_begin.elapsed() > query_timeout { - log::debug!( - "query onlines timeout {:?} ({:?})", - query_begin.elapsed(), - query_timeout - ); - break; + Err(e) => { + log::debug!("query onlines, {}", &e); } - - sleep(1.5).await; } } } @@ -3470,8 +3454,6 @@ pub mod peer_online { ids: &Vec, timeout: std::time::Duration, ) -> ResultType<(Vec, Vec)> { - let query_begin = Instant::now(); - let mut msg_out = RendezvousMessage::new(); msg_out.set_online_request(OnlineRequest { id: Config::get_id(), @@ -3479,24 +3461,28 @@ pub mod peer_online { ..Default::default() }); - loop { - let mut socket = match create_online_stream().await { - Ok(s) => s, - Err(e) => { - log::debug!("Failed to create peers online stream, {e}"); - return Ok((vec![], ids.clone())); - } - }; - // TODO: Use long connections to avoid socket creation - // If we use a Arc>> to hold and reuse the previous socket, - // we may face the following error: - // An established connection was aborted by the software in your host machine. (os error 10053) - if let Err(e) = socket.send(&msg_out).await { - log::debug!("Failed to send peers online states query, {e}"); + let mut socket = match create_online_stream().await { + Ok(s) => s, + Err(e) => { + log::debug!("Failed to create peers online stream, {e}"); return Ok((vec![], ids.clone())); } - if let Some(msg_in) = - crate::common::get_next_nonkeyexchange_msg(&mut socket, None).await + }; + // TODO: Use long connections to avoid socket creation + // If we use a Arc>> to hold and reuse the previous socket, + // we may face the following error: + // An established connection was aborted by the software in your host machine. (os error 10053) + if let Err(e) = socket.send(&msg_out).await { + log::debug!("Failed to send peers online states query, {e}"); + return Ok((vec![], ids.clone())); + } + // Retry for 2 times to get the online response + for _ in 0..2 { + if let Some(msg_in) = crate::common::get_next_nonkeyexchange_msg( + &mut socket, + Some(timeout.as_millis() as _), + ) + .await { match msg_in.union { Some(rendezvous_message::Union::OnlineResponse(online_response)) => { @@ -3522,13 +3508,9 @@ pub mod peer_online { // TODO: Make sure socket closed? bail!("Online stream receives None"); } - - if query_begin.elapsed() > timeout { - bail!("Try query onlines timeout {:?}", &timeout); - } - - sleep(300.0).await; } + + bail!("Failed to query online states, no online response"); } #[cfg(test)] diff --git a/src/flutter.rs b/src/flutter.rs index 717615753b8a..cbeb3e2c3d3a 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -2057,18 +2057,18 @@ pub mod sessions { pub(super) mod async_tasks { use hbb_common::{ bail, - tokio::{ - self, select, - sync::mpsc::{unbounded_channel, UnboundedSender}, - }, + tokio::{self, select}, ResultType, }; use std::{ collections::HashMap, - sync::{Arc, Mutex}, + sync::{ + mpsc::{sync_channel, SyncSender}, + Arc, Mutex, + }, }; - type TxQueryOnlines = UnboundedSender>; + type TxQueryOnlines = SyncSender>; lazy_static::lazy_static! { static ref TX_QUERY_ONLINES: Arc>> = Default::default(); } @@ -2085,20 +2085,18 @@ pub(super) mod async_tasks { #[tokio::main(flavor = "current_thread")] async fn start_flutter_async_runner_() { - let (tx_onlines, mut rx_onlines) = unbounded_channel::>(); + // Only one task is allowed to run at the same time. + let (tx_onlines, rx_onlines) = sync_channel::>(1); TX_QUERY_ONLINES.lock().unwrap().replace(tx_onlines); loop { - select! { - ids = rx_onlines.recv() => { - match ids { - Some(_ids) => { - crate::client::peer_online::query_online_states(_ids, handle_query_onlines).await - } - None => { - break; - } - } + match rx_onlines.recv() { + Ok(ids) => { + crate::client::peer_online::query_online_states(ids, handle_query_onlines).await + } + _ => { + // unreachable! + break; } } } @@ -2106,7 +2104,8 @@ pub(super) mod async_tasks { pub fn query_onlines(ids: Vec) -> ResultType<()> { if let Some(tx) = TX_QUERY_ONLINES.lock().unwrap().as_ref() { - let _ = tx.send(ids)?; + // Ignore if the channel is full. + let _ = tx.try_send(ids)?; } else { bail!("No tx_query_onlines"); } From d2e98cc620dc7553c374372211ffe4c2dcca6001 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Thu, 12 Sep 2024 11:26:01 +0800 Subject: [PATCH 182/541] fix: reduce rebuild for online state (#9331) * fix: reduce rebuild for online state Signed-off-by: fufesou * Query online, update on focus changed Signed-off-by: fufesou * Use to ensure is right Signed-off-by: fufesou * refact, window events on peer view Signed-off-by: fufesou * chore Signed-off-by: fufesou * Remove unused code Signed-off-by: fufesou --------- Signed-off-by: fufesou --- flutter/lib/common/widgets/peers_view.dart | 38 +++++++++++++++++++--- flutter/lib/models/peer_model.dart | 17 +++++++--- 2 files changed, 46 insertions(+), 9 deletions(-) diff --git a/flutter/lib/common/widgets/peers_view.dart b/flutter/lib/common/widgets/peers_view.dart index befc73338ee0..a73ef0f0bd26 100644 --- a/flutter/lib/common/widgets/peers_view.dart +++ b/flutter/lib/common/widgets/peers_view.dart @@ -89,6 +89,7 @@ class _PeersViewState extends State<_PeersView> var _lastChangeTime = DateTime.now(); var _lastQueryPeers = {}; var _lastQueryTime = DateTime.now(); + var _lastWindowRestoreTime = DateTime.now(); var _queryCount = 0; var _exit = false; bool _isActive = true; @@ -117,11 +118,37 @@ class _PeersViewState extends State<_PeersView> @override void onWindowFocus() { _queryCount = 0; + _isActive = true; } @override - void onWindowMinimize() { + 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 + void onWindowMinimize() { + // Window minimize also triggers `onWindowBlur()`. } // This function is required for mobile. @@ -234,10 +261,11 @@ class _PeersViewState extends State<_PeersView> 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); + return buildOnePeer(peers[index], false) + .marginOnly( + right: space, + top: index == 0 ? 0 : space / 2, + bottom: space / 2); }), ) : DesktopScrollWrapper( diff --git a/flutter/lib/models/peer_model.dart b/flutter/lib/models/peer_model.dart index 188dd4e0bdfd..7ab5a2b803ee 100644 --- a/flutter/lib/models/peer_model.dart +++ b/flutter/lib/models/peer_model.dart @@ -194,10 +194,14 @@ class Peers extends ChangeNotifier { } void _updateOnlineState(Map evt) { + int changedCount = 0; evt['onlines'].split(',').forEach((online) { for (var i = 0; i < peers.length; i++) { if (peers[i].id == online) { - peers[i].online = true; + if (!peers[i].online) { + changedCount += 1; + peers[i].online = true; + } } } }); @@ -205,13 +209,18 @@ class Peers extends ChangeNotifier { evt['offlines'].split(',').forEach((offline) { for (var i = 0; i < peers.length; i++) { if (peers[i].id == offline) { - peers[i].online = false; + if (peers[i].online) { + changedCount += 1; + peers[i].online = false; + } } } }); - event = UpdateEvent.online; - notifyListeners(); + if (changedCount > 0) { + event = UpdateEvent.online; + notifyListeners(); + } } void _updatePeers(Map evt) { From cacca7295ca073d134c1e25e99241035c35715c5 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Thu, 12 Sep 2024 14:25:46 +0800 Subject: [PATCH 183/541] fix memory leak on mac because of wrong use of objc, by wrapping autoreleasepool --- src/platform/macos.rs | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/src/platform/macos.rs b/src/platform/macos.rs index 71a1022db445..b3c5546a6fc9 100644 --- a/src/platform/macos.rs +++ b/src/platform/macos.rs @@ -24,6 +24,7 @@ use hbb_common::{ sysinfo::{Pid, Process, ProcessRefreshKind, System}, }; use include_dir::{include_dir, Dir}; +use objc::rc::autoreleasepool; use objc::{class, msg_send, sel, sel_impl}; use scrap::{libc::c_void, quartz::ffi::*}; use std::path::PathBuf; @@ -60,6 +61,10 @@ pub fn major_version() -> u32 { } pub fn is_process_trusted(prompt: bool) -> bool { + autoreleasepool(|| unsafe_is_process_trusted(prompt)) +} + +fn unsafe_is_process_trusted(prompt: bool) -> bool { unsafe { let value = if prompt { YES } else { NO }; let value: id = msg_send![class!(NSNumber), numberWithBool: value]; @@ -79,10 +84,14 @@ pub fn is_can_input_monitoring(prompt: bool) -> bool { } } +pub fn is_can_screen_recording(prompt: bool) -> bool { + autoreleasepool(|| unsafe_is_can_screen_recording(prompt)) +} + // macOS >= 10.15 // https://stackoverflow.com/questions/56597221/detecting-screen-recording-settings-on-macos-catalina/ // remove just one app from all the permissions: tccutil reset All com.carriez.rustdesk -pub fn is_can_screen_recording(prompt: bool) -> bool { +fn unsafe_is_can_screen_recording(prompt: bool) -> bool { // we got some report that we show no permission even after set it, so we try to use new api for screen recording check // the new api is only available on macOS >= 10.15, but on stackoverflow, some people said it works on >= 10.16 (crash on 10.15), // but also some said it has bug on 10.16, so we just use it on 11.0. @@ -297,6 +306,10 @@ pub fn get_cursor_pos() -> Option<(i32, i32)> { } pub fn get_focused_display(displays: Vec) -> Option { + autoreleasepool(|| unsafe_get_focused_display(displays)) +} + +fn unsafe_get_focused_display(displays: Vec) -> Option { unsafe { let main_screen: id = msg_send![class!(NSScreen), mainScreen]; let screen: id = msg_send![main_screen, deviceDescription]; @@ -311,6 +324,10 @@ pub fn get_focused_display(displays: Vec) -> Option { } pub fn get_cursor() -> ResultType> { + autoreleasepool(|| unsafe_get_cursor()) +} + +fn unsafe_get_cursor() -> ResultType> { unsafe { let seed = CGSCurrentCursorSeed(); if seed == LATEST_SEED { @@ -375,8 +392,12 @@ fn get_cursor_id() -> ResultType<(id, u64)> { } } -// https://github.com/stweil/OSXvnc/blob/master/OSXvnc-server/mousecursor.c pub fn get_cursor_data(hcursor: u64) -> ResultType { + autoreleasepool(|| unsafe_get_cursor_data(hcursor)) +} + +// https://github.com/stweil/OSXvnc/blob/master/OSXvnc-server/mousecursor.c +fn unsafe_get_cursor_data(hcursor: u64) -> ResultType { unsafe { let (c, hcursor2) = get_cursor_id()?; if hcursor != hcursor2 { From c358399eca055bfdc80e34f554c1d930684c519c Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Thu, 12 Sep 2024 14:44:40 +0800 Subject: [PATCH 184/541] refact: reduce try_get_displays() on login (#9333) * refact: reduce try_get_displays() on login Signed-off-by: fufesou * Function rename Signed-off-by: fufesou --------- Signed-off-by: fufesou --- src/server/connection.rs | 34 +++++++++++++--------------------- src/server/display_service.rs | 8 ++++++-- 2 files changed, 19 insertions(+), 23 deletions(-) diff --git a/src/server/connection.rs b/src/server/connection.rs index 3a2b0a22e914..7181478b3d0d 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -1279,29 +1279,9 @@ impl Connection { self.send(msg_out).await; } - #[cfg(not(any(target_os = "android", target_os = "ios")))] - { - #[cfg(not(windows))] - let displays = display_service::try_get_displays(); - #[cfg(windows)] - let displays = display_service::try_get_displays_add_amyuni_headless(); - pi.resolutions = Some(SupportedResolutions { - resolutions: displays - .map(|displays| { - displays - .get(self.display_idx) - .map(|d| crate::platform::resolutions(&d.name())) - .unwrap_or(vec![]) - }) - .unwrap_or(vec![]), - ..Default::default() - }) - .into(); - } - try_activate_screen(); - match super::display_service::update_get_sync_displays().await { + match super::display_service::update_get_sync_displays_on_login().await { Err(err) => { res.set_error(format!("{}", err)); } @@ -1314,6 +1294,18 @@ impl Connection { } pi.displays = displays; pi.current_display = self.display_idx as _; + #[cfg(not(any(target_os = "android", target_os = "ios")))] + { + pi.resolutions = Some(SupportedResolutions { + resolutions: pi + .displays + .get(self.display_idx) + .map(|d| crate::platform::resolutions(&d.name)) + .unwrap_or(vec![]), + ..Default::default() + }) + .into(); + } res.set_peer_info(pi); sub_service = true; diff --git a/src/server/display_service.rs b/src/server/display_service.rs index e099e25a096a..98b42a5facea 100644 --- a/src/server/display_service.rs +++ b/src/server/display_service.rs @@ -344,14 +344,18 @@ pub fn is_inited_msg() -> Option { None } -pub async fn update_get_sync_displays() -> ResultType> { +pub async fn update_get_sync_displays_on_login() -> ResultType> { #[cfg(target_os = "linux")] { if !is_x11() { return super::wayland::get_displays().await; } } - check_update_displays(&try_get_displays()?); + #[cfg(not(windows))] + let displays = display_service::try_get_displays(); + #[cfg(windows)] + let displays = display_service::try_get_displays_add_amyuni_headless(); + check_update_displays(&displays?); Ok(SYNC_DISPLAYS.lock().unwrap().displays.clone()) } From 0b3e7bf33e5651c075870309ad7d3bb1816a5163 Mon Sep 17 00:00:00 2001 From: 21pages Date: Thu, 12 Sep 2024 17:34:54 +0800 Subject: [PATCH 185/541] update hwcodec, fix linux ci (#9335) Signed-off-by: 21pages --- Cargo.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index b2bd08d436a6..3e793ddb791d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3045,7 +3045,7 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hwcodec" version = "0.7.0" -source = "git+https://github.com/rustdesk-org/hwcodec#9e8b6efd8e5d904b5325597a271ebe78f5a74f3b" +source = "git+https://github.com/rustdesk-org/hwcodec#f74410edec91435252b8394c38f8eeca87ad2a26" dependencies = [ "bindgen 0.59.2", "cc", From a8f1a66043cd98e44bb8df34dcb1eb6d77b2844b Mon Sep 17 00:00:00 2001 From: m-hume Date: Fri, 13 Sep 2024 01:06:40 +0100 Subject: [PATCH 186/541] Trim whitespace from Import server config (#9341) --- flutter/lib/common/widgets/setting_widgets.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/lib/common/widgets/setting_widgets.dart b/flutter/lib/common/widgets/setting_widgets.dart index 5bcb73a4c5e1..b76bd3f55c02 100644 --- a/flutter/lib/common/widgets/setting_widgets.dart +++ b/flutter/lib/common/widgets/setting_widgets.dart @@ -184,7 +184,7 @@ List ServerConfigImportExportWidgets( ) { import() { Clipboard.getData(Clipboard.kTextPlain).then((value) { - importConfig(controllers, errMsgs, value?.text); + importConfig(controllers, errMsgs, value?.text.trim()); }); } From 9f9a22ec63c22e0d82418415c91a614f2f7c0235 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Fri, 13 Sep 2024 08:46:21 +0800 Subject: [PATCH 187/541] uppercase for all --- flutter/lib/common/widgets/custom_password.dart | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/flutter/lib/common/widgets/custom_password.dart b/flutter/lib/common/widgets/custom_password.dart index 99ece2434bf4..1fa6e3645d9c 100644 --- a/flutter/lib/common/widgets/custom_password.dart +++ b/flutter/lib/common/widgets/custom_password.dart @@ -14,7 +14,11 @@ class UppercaseValidationRule extends ValidationRule { String get name => translate('uppercase'); @override 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; + }); } } From d65d3b7326b6d428a1e8fc6be2b6d0dd9a883151 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Fri, 13 Sep 2024 09:21:50 +0800 Subject: [PATCH 188/541] fix ci --- flutter/lib/common.dart | 1 + flutter/lib/common/widgets/setting_widgets.dart | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index efcff4f83f4c..7b6ec4ae5fc8 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -3154,6 +3154,7 @@ class _ReconnectCountDownButtonState extends State<_ReconnectCountDownButton> { importConfig(List? controllers, List? errMsgs, String? text) { + text = text?.trim(); if (text != null && text.isNotEmpty) { try { final sc = ServerConfig.decode(text); diff --git a/flutter/lib/common/widgets/setting_widgets.dart b/flutter/lib/common/widgets/setting_widgets.dart index b76bd3f55c02..5bcb73a4c5e1 100644 --- a/flutter/lib/common/widgets/setting_widgets.dart +++ b/flutter/lib/common/widgets/setting_widgets.dart @@ -184,7 +184,7 @@ List ServerConfigImportExportWidgets( ) { import() { Clipboard.getData(Clipboard.kTextPlain).then((value) { - importConfig(controllers, errMsgs, value?.text.trim()); + importConfig(controllers, errMsgs, value?.text); }); } From ab246fdcbf877dc84456af921680b9925cbd3ff1 Mon Sep 17 00:00:00 2001 From: 21pages Date: Fri, 13 Sep 2024 09:29:00 +0800 Subject: [PATCH 189/541] password lowercase check like uppercase (#9343) Signed-off-by: 21pages --- flutter/lib/common/widgets/custom_password.dart | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/flutter/lib/common/widgets/custom_password.dart b/flutter/lib/common/widgets/custom_password.dart index 1fa6e3645d9c..dafc23b448b9 100644 --- a/flutter/lib/common/widgets/custom_password.dart +++ b/flutter/lib/common/widgets/custom_password.dart @@ -28,7 +28,11 @@ class LowercaseValidationRule extends ValidationRule { @override 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; + }); } } From 179b562472a1edf9d68e97a2ed11926c8779b881 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Fri, 13 Sep 2024 15:41:29 +0800 Subject: [PATCH 190/541] another leak --- src/platform/windows.cc | 1 + 1 file changed, 1 insertion(+) diff --git a/src/platform/windows.cc b/src/platform/windows.cc index 7ed76d2e4bee..9d284aea2d46 100644 --- a/src/platform/windows.cc +++ b/src/platform/windows.cc @@ -549,6 +549,7 @@ extern "C" continue; if (!stricmp(info.pWinStationName, "console")) { + WTSFreeMemory(pInfos); return info.SessionId; } if (!strnicmp(info.pWinStationName, rdp, nrdp)) From 2e7bd26e4c30ccb04eb13575af429dc11ac30302 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Fri, 13 Sep 2024 15:42:51 +0800 Subject: [PATCH 191/541] fix leak fix --- src/platform/windows.cc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/platform/windows.cc b/src/platform/windows.cc index 9d284aea2d46..9ee3c1f5c9a6 100644 --- a/src/platform/windows.cc +++ b/src/platform/windows.cc @@ -549,8 +549,9 @@ extern "C" continue; if (!stricmp(info.pWinStationName, "console")) { + auto id = info.SessionId; WTSFreeMemory(pInfos); - return info.SessionId; + return id; } if (!strnicmp(info.pWinStationName, rdp, nrdp)) { From 81fc22a1568b35b5c1f355f57face8a96af6c6d8 Mon Sep 17 00:00:00 2001 From: Alex Rijckaert Date: Fri, 13 Sep 2024 15:04:04 +0200 Subject: [PATCH 192/541] Update nl.rs (#9344) * Update nl.rs * Update nl.rs file updated after adjusting (as test) @FastAct to RijckAlex (same person, new account) --- src/lang/nl.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lang/nl.rs b/src/lang/nl.rs index 9ae7d0839f92..78b36b2e33b5 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -31,8 +31,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("ID/Relay Server", "ID/Relay Server"), ("Import server config", "Importeer Serverconfiguratie"), ("Export Server Config", "Exporteer Serverconfiguratie"), - ("Import server configuration successfully", "Importeren serverconfiguratie succesvol"), - ("Export server configuration successfully", "Exporteren serverconfiguratie succesvol"), + ("Import server configuration successfully", "Importeren serverconfiguratie is geslaagd"), + ("Export server configuration successfully", "Exporteren serverconfiguratie is geslaagd"), ("Invalid server configuration", "Ongeldige Serverconfiguratie"), ("Clipboard is empty", "Klembord is leeg"), ("Stop service", "Stop service"), @@ -80,7 +80,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Connection in progress. Please wait.", "Verbinding in uitvoering. Even geduld a.u.b."), ("Please try 1 minute later", "Probeer 1 minuut later"), ("Login Error", "Login Fout"), - ("Successful", "Succesvol"), + ("Successful", "Geslaagd"), ("Connected, waiting for image...", "Verbonden, wacht op beeld..."), ("Name", "Naam"), ("Type", "Type"), From 40af9dc78beb26ea9444859de4676ff974b34dab Mon Sep 17 00:00:00 2001 From: 21pages Date: Sat, 14 Sep 2024 09:59:14 +0800 Subject: [PATCH 193/541] not run window focus service on wayland (#9354) Signed-off-by: 21pages --- libs/scrap/src/x11/server.rs | 14 ++++++++++---- src/server.rs | 5 ++++- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/libs/scrap/src/x11/server.rs b/libs/scrap/src/x11/server.rs index e2ffdc74b4f6..f9983f7cf2a2 100644 --- a/libs/scrap/src/x11/server.rs +++ b/libs/scrap/src/x11/server.rs @@ -1,3 +1,4 @@ +use hbb_common::libc; use std::ptr; use std::rc::Rc; @@ -99,11 +100,16 @@ unsafe fn check_x11_shm_available(c: *mut xcb_connection_t) -> Result<(), Error> if reply.is_null() { // TODO: Should seperate SHM disabled from SHM not supported? return Err(Error::UnsupportedExtension); - } else if e.is_null() { - return Ok(()); } else { - // TODO: Does "This request does never generate any errors" in manual means `e` is never set, so we would never reach here? - return Err(Error::Generic); + // https://github.com/FFmpeg/FFmpeg/blob/6229e4ac425b4566446edefb67d5c225eb397b58/libavdevice/xcbgrab.c#L229 + libc::free(reply as *mut _); + if e.is_null() { + return Ok(()); + } else { + libc::free(e as *mut _); + // TODO: Does "This request does never generate any errors" in manual means `e` is never set, so we would never reach here? + return Err(Error::Generic); + } } } diff --git a/src/server.rs b/src/server.rs index a973ba6aef99..74bda41ce053 100644 --- a/src/server.rs +++ b/src/server.rs @@ -106,7 +106,10 @@ pub fn new() -> ServerPtr { if !display_service::capture_cursor_embedded() { server.add_service(Box::new(input_service::new_cursor())); server.add_service(Box::new(input_service::new_pos())); - server.add_service(Box::new(input_service::new_window_focus())); + if scrap::is_x11() { + // wayland does not support multiple displays currently + server.add_service(Box::new(input_service::new_window_focus())); + } } } Arc::new(RwLock::new(server)) From d9ea717056c14cd0b37239d36b1102840d70d8f2 Mon Sep 17 00:00:00 2001 From: 21pages Date: Sat, 14 Sep 2024 10:03:50 +0800 Subject: [PATCH 194/541] fix last commit (#9355) Signed-off-by: 21pages --- src/server.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/server.rs b/src/server.rs index 74bda41ce053..02522db96841 100644 --- a/src/server.rs +++ b/src/server.rs @@ -106,6 +106,7 @@ pub fn new() -> ServerPtr { if !display_service::capture_cursor_embedded() { server.add_service(Box::new(input_service::new_cursor())); server.add_service(Box::new(input_service::new_pos())); + #[cfg(target_os = "linux")] if scrap::is_x11() { // wayland does not support multiple displays currently server.add_service(Box::new(input_service::new_window_focus())); From f4c038ea933dc4394c71dbe1014ae999f71d0641 Mon Sep 17 00:00:00 2001 From: 21pages Date: Sun, 15 Sep 2024 14:33:59 +0800 Subject: [PATCH 195/541] update appindicator and recommends install it (#9364) Signed-off-by: 21pages --- .github/workflows/flutter-build.yml | 8 ++++---- .github/workflows/playground.yml | 2 +- build.py | 3 ++- res/rpm-flutter-suse.spec | 3 ++- res/rpm-flutter.spec | 3 ++- res/rpm-suse.spec | 3 ++- res/rpm.spec | 3 ++- 7 files changed, 15 insertions(+), 10 deletions(-) diff --git a/.github/workflows/flutter-build.yml b/.github/workflows/flutter-build.yml index d7a36b4005a2..b5f8707874e8 100644 --- a/.github/workflows/flutter-build.yml +++ b/.github/workflows/flutter-build.yml @@ -884,7 +884,7 @@ jobs: git \ g++ \ g++-multilib \ - libappindicator3-dev \ + libayatana-appindicator3-dev \ libasound2-dev \ libc6-dev \ libclang-10-dev \ @@ -1147,7 +1147,7 @@ jobs: git \ g++ \ g++-multilib \ - libappindicator3-dev \ + libayatana-appindicator3-dev \ libasound2-dev \ libc6-dev \ libclang-10-dev \ @@ -1424,7 +1424,7 @@ jobs: gcc \ git \ g++ \ - libappindicator3-dev \ + libayatana-appindicator3-dev \ libasound2-dev \ libclang-10-dev \ libgstreamer1.0-dev \ @@ -1681,7 +1681,7 @@ jobs: gcc \ git \ g++ \ - libappindicator3-dev \ + libayatana-appindicator3-dev \ libasound2-dev \ libclang-dev \ libdbus-1-dev \ diff --git a/.github/workflows/playground.yml b/.github/workflows/playground.yml index fb7b89614506..205ce8f1ed54 100644 --- a/.github/workflows/playground.yml +++ b/.github/workflows/playground.yml @@ -262,7 +262,7 @@ jobs: git \ g++ \ g++-multilib \ - libappindicator3-dev \ + libayatana-appindicator3-dev\ libasound2-dev \ libc6-dev \ libclang-10-dev \ diff --git a/build.py b/build.py index 389ee33b6cd6..be13207ffeab 100755 --- a/build.py +++ b/build.py @@ -287,7 +287,8 @@ def generate_control_file(version): Architecture: %s Maintainer: rustdesk 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%s +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. """ % (version, get_deb_arch(), get_deb_extra_depends()) diff --git a/res/rpm-flutter-suse.spec b/res/rpm-flutter-suse.spec index 2995c54d4354..c2c5be1f188c 100644 --- a/res/rpm-flutter-suse.spec +++ b/res/rpm-flutter-suse.spec @@ -3,7 +3,8 @@ Version: 1.3.1 Release: 0 Summary: RPM package License: GPL-3.0 -Requires: gtk3 libxcb1 xdotool libXfixes3 alsa-utils libXtst6 libappindicator-gtk3 libvdpau1 libva2 pam gstreamer-plugins-base gstreamer-plugin-pipewire +Requires: gtk3 libxcb1 xdotool libXfixes3 alsa-utils libXtst6 libvdpau1 libva2 pam gstreamer-plugins-base gstreamer-plugin-pipewire +Recommends: libayatana-appindicator3-1 Provides: libdesktop_drop_plugin.so()(64bit), libdesktop_multi_window_plugin.so()(64bit), libfile_selector_linux_plugin.so()(64bit), libflutter_custom_cursor_plugin.so()(64bit), libflutter_linux_gtk.so()(64bit), libscreen_retriever_plugin.so()(64bit), libtray_manager_plugin.so()(64bit), liburl_launcher_linux_plugin.so()(64bit), libwindow_manager_plugin.so()(64bit), libwindow_size_plugin.so()(64bit), libtexture_rgba_renderer_plugin.so()(64bit) %description diff --git a/res/rpm-flutter.spec b/res/rpm-flutter.spec index 48a7e2ac0001..33a1314ccca6 100644 --- a/res/rpm-flutter.spec +++ b/res/rpm-flutter.spec @@ -3,7 +3,8 @@ Version: 1.3.1 Release: 0 Summary: RPM package License: GPL-3.0 -Requires: gtk3 libxcb libxdo libXfixes alsa-lib libappindicator-gtk3 libvdpau libva pam gstreamer1-plugins-base +Requires: gtk3 libxcb libxdo libXfixes alsa-lib libvdpau libva pam gstreamer1-plugins-base +Recommends: libayatana-appindicator-gtk3 Provides: libdesktop_drop_plugin.so()(64bit), libdesktop_multi_window_plugin.so()(64bit), libfile_selector_linux_plugin.so()(64bit), libflutter_custom_cursor_plugin.so()(64bit), libflutter_linux_gtk.so()(64bit), libscreen_retriever_plugin.so()(64bit), libtray_manager_plugin.so()(64bit), liburl_launcher_linux_plugin.so()(64bit), libwindow_manager_plugin.so()(64bit), libwindow_size_plugin.so()(64bit), libtexture_rgba_renderer_plugin.so()(64bit) %description diff --git a/res/rpm-suse.spec b/res/rpm-suse.spec index d84e14812387..1d6a94b131bb 100644 --- a/res/rpm-suse.spec +++ b/res/rpm-suse.spec @@ -3,7 +3,8 @@ Version: 1.1.9 Release: 0 Summary: RPM package License: GPL-3.0 -Requires: gtk3 libxcb1 xdotool libXfixes3 alsa-utils libXtst6 libayatana-appindicator3-1 libvdpau1 libva2 pam gstreamer-plugins-base gstreamer-plugin-pipewire +Requires: gtk3 libxcb1 xdotool libXfixes3 alsa-utils libXtst6 libvdpau1 libva2 pam gstreamer-plugins-base gstreamer-plugin-pipewire +Recommends: libayatana-appindicator3-1 %description The best open-source remote desktop client software, written in Rust. diff --git a/res/rpm.spec b/res/rpm.spec index 8c99f9bb0b41..11dccc84209c 100644 --- a/res/rpm.spec +++ b/res/rpm.spec @@ -3,7 +3,8 @@ Version: 1.3.1 Release: 0 Summary: RPM package License: GPL-3.0 -Requires: gtk3 libxcb libxdo libXfixes alsa-lib libappindicator libvdpau1 libva2 pam gstreamer1-plugins-base +Requires: gtk3 libxcb libxdo libXfixes alsa-lib libvdpau1 libva2 pam gstreamer1-plugins-base +Recommends: libayatana-appindicator-gtk3 %description The best open-source remote desktop client software, written in Rust. From c5038b1a78fe6d0ef6f4ef46aae180896859fe7f Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Mon, 16 Sep 2024 15:57:40 +0800 Subject: [PATCH 196/541] Fix/virtual display do not plug out if not plugged in (#9372) * fix: win VD, do not plug out if not plugged in Signed-off-by: fufesou * Forcibly virtual display on clicking button "-" Signed-off-by: fufesou --------- Signed-off-by: fufesou --- src/privacy_mode/win_virtual_display.rs | 11 ++-- src/server/connection.rs | 2 +- src/virtual_display_manager.rs | 86 ++++++++++++++++++++----- 3 files changed, 78 insertions(+), 21 deletions(-) diff --git a/src/privacy_mode/win_virtual_display.rs b/src/privacy_mode/win_virtual_display.rs index 25997f03671c..782d7ed75a87 100644 --- a/src/privacy_mode/win_virtual_display.rs +++ b/src/privacy_mode/win_virtual_display.rs @@ -150,8 +150,11 @@ impl PrivacyModeImpl { } fn restore_plug_out_monitor(&mut self) { - let _ = - virtual_display_manager::plug_out_monitor_indices(&self.virtual_displays_added, true); + let _ = virtual_display_manager::plug_out_monitor_indices( + &self.virtual_displays_added, + true, + false, + ); self.virtual_displays_added.clear(); } @@ -312,7 +315,7 @@ impl PrivacyModeImpl { // No physical displays, no need to use the privacy mode. if self.displays.is_empty() { - virtual_display_manager::plug_out_monitor_indices(&displays, false)?; + virtual_display_manager::plug_out_monitor_indices(&displays, false, false)?; bail!(NO_PHYSICAL_DISPLAYS); } @@ -509,7 +512,7 @@ pub fn restore_reg_connectivity(plug_out_monitors: bool) { return; } if plug_out_monitors { - let _ = virtual_display_manager::plug_out_monitor(-1, true); + let _ = virtual_display_manager::plug_out_monitor(-1, true, false); } if let Ok(reg_recovery) = serde_json::from_str::(&config_recovery_value) diff --git a/src/server/connection.rs b/src/server/connection.rs index 7181478b3d0d..e351b0d5006b 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -2713,7 +2713,7 @@ impl Connection { } } } else { - if let Err(e) = virtual_display_manager::plug_out_monitor(t.display, false) { + if let Err(e) = virtual_display_manager::plug_out_monitor(t.display, false, true) { log::error!("Failed to plug out virtual display {}: {}", t.display, e); self.send(make_msg(format!( "Failed to plug out virtual displays: {}", diff --git a/src/virtual_display_manager.rs b/src/virtual_display_manager.rs index 138087c75e86..41e5b3fc83cb 100644 --- a/src/virtual_display_manager.rs +++ b/src/virtual_display_manager.rs @@ -8,6 +8,7 @@ pub const AMYUNI_IDD_DEVICE_STRING: &'static str = "USB Mobile Monitor Virtual D const IDD_IMPL: &str = IDD_IMPL_AMYUNI; const IDD_IMPL_RUSTDESK: &str = "rustdesk_idd"; const IDD_IMPL_AMYUNI: &str = "amyuni_idd"; +const IDD_PLUG_OUT_ALL_INDEX: i32 = -1; pub fn is_amyuni_idd() -> bool { IDD_IMPL == IDD_IMPL_AMYUNI @@ -76,17 +77,17 @@ pub fn plug_in_monitor(idx: u32, modes: Vec) -> Re } } -pub fn plug_out_monitor(index: i32, force_all: bool) -> ResultType<()> { +pub fn plug_out_monitor(index: i32, force_all: bool, force_one: bool) -> ResultType<()> { match IDD_IMPL { IDD_IMPL_RUSTDESK => { - let indices = if index == -1 { + let indices = if index == IDD_PLUG_OUT_ALL_INDEX { rustdesk_idd::get_virtual_displays() } else { vec![index as _] }; rustdesk_idd::plug_out_peer_request(&indices) } - IDD_IMPL_AMYUNI => amyuni_idd::plug_out_monitor(index, force_all), + IDD_IMPL_AMYUNI => amyuni_idd::plug_out_monitor(index, force_all, force_one), _ => bail!("Unsupported virtual display implementation."), } } @@ -102,12 +103,16 @@ pub fn plug_in_peer_request(modes: Vec>) -> Re } } -pub fn plug_out_monitor_indices(indices: &[u32], force_all: bool) -> ResultType<()> { +pub fn plug_out_monitor_indices( + indices: &[u32], + force_all: bool, + force_one: bool, +) -> ResultType<()> { match IDD_IMPL { IDD_IMPL_RUSTDESK => rustdesk_idd::plug_out_peer_request(indices), IDD_IMPL_AMYUNI => { for _idx in indices.iter() { - amyuni_idd::plug_out_monitor(0, force_all)?; + amyuni_idd::plug_out_monitor(0, force_all, force_one)?; } Ok(()) } @@ -382,7 +387,7 @@ pub mod amyuni_idd { use hbb_common::{bail, lazy_static, log, tokio::time::Instant, ResultType}; use std::{ ptr::null_mut, - sync::{Arc, Mutex}, + sync::{atomic, Arc, Mutex}, time::Duration, }; use winapi::{ @@ -405,6 +410,14 @@ pub mod amyuni_idd { static ref LOCK: Arc> = Default::default(); static ref LAST_PLUG_IN_HEADLESS_TIME: Arc>> = Arc::new(Mutex::new(None)); } + const VIRTUAL_DISPLAY_MAX_COUNT: usize = 4; + // The count of virtual displays plugged in. + // This count is not accurate, because: + // 1. The virtual display driver may also be controlled by other processes. + // 2. RustDesk may crash and restart, but the virtual displays are kept. + // + // to-do: Maybe a better way is to add an option asking the user if plug out all virtual displays on disconnect. + static VIRTUAL_DISPLAY_COUNT: atomic::AtomicUsize = atomic::AtomicUsize::new(0); fn get_deviceinstaller64_work_dir() -> ResultType>> { let cur_exe = std::env::current_exe()?; @@ -510,7 +523,7 @@ pub mod amyuni_idd { pub fn reset_all() -> ResultType<()> { let _ = crate::privacy_mode::turn_off_privacy(0, None); - let _ = plug_out_monitor(-1, true); + let _ = plug_out_monitor(super::IDD_PLUG_OUT_ALL_INDEX, true, false); *LAST_PLUG_IN_HEADLESS_TIME.lock().unwrap() = None; Ok(()) } @@ -522,6 +535,18 @@ pub mod amyuni_idd { unsafe { win_device::device_io_control(&INTERFACE_GUID, PLUG_MONITOR_IO_CONTROL_CDOE, &cmd, 0)?; } + // No need to consider concurrency here. + if add { + // If the monitor is plugged in, increase the count. + // Though there's already a check of `VIRTUAL_DISPLAY_MAX_COUNT`, it's still better to check here for double ensure. + if VIRTUAL_DISPLAY_COUNT.load(atomic::Ordering::SeqCst) < VIRTUAL_DISPLAY_MAX_COUNT { + VIRTUAL_DISPLAY_COUNT.fetch_add(1, atomic::Ordering::SeqCst); + } + } else { + if VIRTUAL_DISPLAY_COUNT.load(atomic::Ordering::SeqCst) > 0 { + VIRTUAL_DISPLAY_COUNT.fetch_sub(1, atomic::Ordering::SeqCst); + } + } Ok(()) } @@ -607,44 +632,73 @@ pub mod amyuni_idd { bail!("Failed to install driver."); } - if get_monitor_count() == 4 { - bail!("There are already 4 monitors plugged in."); + if get_monitor_count() == VIRTUAL_DISPLAY_MAX_COUNT { + bail!("There are already {VIRTUAL_DISPLAY_MAX_COUNT} monitors plugged in."); } plug_in_monitor_(true, is_async) } - pub fn plug_out_monitor(index: i32, force_all: bool) -> ResultType<()> { - let all_count = windows::get_device_names(None).len(); + // `index` the display index to plug out. -1 means plug out all. + // `force_all` is used to forcibly plug out all virtual displays. + // `force_one` is used to forcibly plug out one virtual display managed by other processes + // if there're no virtual displays managed by RustDesk. + pub fn plug_out_monitor(index: i32, force_all: bool, force_one: bool) -> ResultType<()> { + let plug_out_all = index == super::IDD_PLUG_OUT_ALL_INDEX; + // If `plug_out_all and force_all` is true, forcibly plug out all virtual displays. + // Though the driver may be controlled by other processes, + // we still forcibly plug out all virtual displays. + // + // 1. RustDesk plug in 2 virtual displays. (RustDesk) + // 2. Other process plug out all virtual displays. (User mannually) + // 3. Other process plug in 1 virtual display. (User mannually) + // 4. RustDesk plug out all virtual displays in this call. (RustDesk disconnect) + // + // This is not a normal scenario, RustDesk will plug out virtual display unexpectedly. + let mut plug_in_count = VIRTUAL_DISPLAY_COUNT.load(atomic::Ordering::Relaxed); let amyuni_count = get_monitor_count(); + if !plug_out_all { + if plug_in_count == 0 && amyuni_count > 0 { + if force_one { + plug_in_count = 1; + } else { + bail!("The virtual display is managed by other processes."); + } + } + } else { + // Ignore the message if trying to plug out all virtual displays. + } + + let all_count = windows::get_device_names(None).len(); let mut to_plug_out_count = match all_count { 0 => return Ok(()), 1 => { - if amyuni_count == 0 { + if plug_in_count == 0 { bail!("No virtual displays to plug out.") } else { if force_all { 1 } else { - bail!("This only virtual display cannot be pulled out.") + bail!("This only virtual display cannot be plugged out.") } } } _ => { - if all_count == amyuni_count { + if all_count == plug_in_count { if force_all { all_count } else { all_count - 1 } } else { - amyuni_count + plug_in_count } } }; - if to_plug_out_count != 0 && index != -1 { + if to_plug_out_count != 0 && !plug_out_all { to_plug_out_count = 1; } + for _i in 0..to_plug_out_count { let _ = plug_monitor_(false); } From 8a8f708c3e7bce90860fb68a12e72c422eecab39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=8A=E9=A4=85=E3=81=AECreeeper?= <56744841+creeper-0910@users.noreply.github.com> Date: Tue, 17 Sep 2024 13:32:32 +0900 Subject: [PATCH 197/541] update ja.rs (#9376) --- src/lang/ja.rs | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/lang/ja.rs b/src/lang/ja.rs index ffb93e379f78..46a7e9271e55 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -630,19 +630,19 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("cancel-2fa-confirm-tip", "本当に二要素認証をキャンセルしますか?"), ("cancel-bot-confirm-tip", "本当にTelegram Botをキャンセルしますか?"), ("About RustDesk", "RustDeskについて"), - ("Send clipboard keystrokes", ""), - ("network_error_tip", ""), - ("Unlock with PIN", ""), - ("Requires at least {} characters", ""), - ("Wrong PIN", ""), - ("Set PIN", ""), - ("Enable trusted devices", ""), - ("Manage trusted devices", ""), - ("Platform", ""), - ("Days remaining", ""), - ("enable-trusted-devices-tip", ""), - ("Parent directory", ""), - ("Resume", ""), - ("Invalid file name", ""), + ("Send clipboard keystrokes", "クリップボードの内容をキー入力として送信する"), + ("network_error_tip", "ネットワーク接続を確認し、再度お試しください。"), + ("Unlock with PIN", "PINでロック解除"), + ("Requires at least {} characters", "最低でも{}文字必要です"), + ("Wrong PIN", "PINが間違っています"), + ("Set PIN", "PINを設定"), + ("Enable trusted devices", "承認済デバイスを有効化"), + ("Manage trusted devices", "承認済デバイスの管理"), + ("Platform", "プラットフォーム"), + ("Days remaining", "残り日数"), + ("enable-trusted-devices-tip", "承認済デバイスで2FAチェックをスキップします。"), + ("Parent directory", "親ディレクトリ"), + ("Resume", "再開"), + ("Invalid file name", "無効なファイル名"), ].iter().cloned().collect(); } From 29c3b29bda5b6c6c2d682d25ae36e943c32dfed2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=8A=E9=A4=85=E3=81=AECreeeper?= <56744841+creeper-0910@users.noreply.github.com> Date: Tue, 17 Sep 2024 14:26:49 +0900 Subject: [PATCH 198/541] Fix ja.rs typo (#9378) --- src/lang/ja.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 46a7e9271e55..9d0f6ea05117 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -19,7 +19,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recent sessions", "最近のセッション"), ("Address book", "アドレス帳"), ("Confirmation", "確認"), - ("TCP tunneling", "TXPトンネリング"), + ("TCP tunneling", "TCPトンネリング"), ("Remove", "削除"), ("Refresh random password", "ランダムパスワードを再生成"), ("Set your own password", "パスワードを設定"), From 49ce4edb8a44ca40d5d89d757ed4c7b42073dccd Mon Sep 17 00:00:00 2001 From: Lumiphare <58658634+Lumiphare@users.noreply.github.com> Date: Wed, 18 Sep 2024 10:39:26 +0800 Subject: [PATCH 199/541] Chinese versions of CONTRIBUTING.md and CODE_OF_CONDUCT-ZH.md (#9386) * Update CONTRIBUTING.md links to point to the Chinese version * translated with AI assistance and manual refinement * Adapted from the official Chinese translation of the Contributor Covenant --------- Co-authored-by: sea --- docs/CODE_OF_CONDUCT-ZH.md | 87 ++++++++++++++++++++++++++++++++++++++ docs/CONTRIBUTING-ZH.md | 32 ++++++++++++++ docs/README-ZH.md | 2 +- 3 files changed, 120 insertions(+), 1 deletion(-) create mode 100644 docs/CODE_OF_CONDUCT-ZH.md create mode 100644 docs/CONTRIBUTING-ZH.md diff --git a/docs/CODE_OF_CONDUCT-ZH.md b/docs/CODE_OF_CONDUCT-ZH.md new file mode 100644 index 000000000000..0877ab20f246 --- /dev/null +++ b/docs/CODE_OF_CONDUCT-ZH.md @@ -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 \ No newline at end of file diff --git a/docs/CONTRIBUTING-ZH.md b/docs/CONTRIBUTING-ZH.md new file mode 100644 index 000000000000..718cdac69bb5 --- /dev/null +++ b/docs/CONTRIBUTING-ZH.md @@ -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) 进行交流。 diff --git a/docs/README-ZH.md b/docs/README-ZH.md index 54b9c29a1417..0460384ab42f 100644 --- a/docs/README-ZH.md +++ b/docs/README-ZH.md @@ -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) -RustDesk 期待各位的贡献. 如何参与开发? 详情请看 [CONTRIBUTING.md](CONTRIBUTING.md). +RustDesk 期待各位的贡献. 如何参与开发? 详情请看 [CONTRIBUTING-ZH.md](CONTRIBUTING-ZH.md). [**FAQ**](https://github.com/rustdesk/rustdesk/wiki/FAQ) From cc288272d3334d133e47a0c1696cf42de6d2bf25 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Wed, 18 Sep 2024 12:18:26 +0800 Subject: [PATCH 200/541] OPTION_ONE_WAY_CLIPBOARD_REDIRECTION, OPTION_ENABLE_CLIPBOARD_INIT_SYNC, OPTION_ALLOW_LOGON_SCREEN_PASSWORD, OPTION_ONE_WAY_FILE_TRANSFER, --- libs/hbb_common/src/config.rs | 8 +++++++ libs/hbb_common/src/platform/mod.rs | 6 +++++ src/client/io_loop.rs | 23 +++++++++++------- src/common.rs | 10 ++++++++ src/server/connection.rs | 37 +++++++++++++++++++++++------ src/ui_cm_interface.rs | 2 +- src/ui_interface.rs | 7 +----- 7 files changed, 70 insertions(+), 23 deletions(-) diff --git a/libs/hbb_common/src/config.rs b/libs/hbb_common/src/config.rs index c118070dd851..0e91ecf42c3f 100644 --- a/libs/hbb_common/src/config.rs +++ b/libs/hbb_common/src/config.rs @@ -2218,6 +2218,10 @@ pub mod keys { pub const OPTION_HIDE_HELP_CARDS: &str = "hide-help-cards"; pub const OPTION_DEFAULT_CONNECT_PASSWORD: &str = "default-connect-password"; pub const OPTION_HIDE_TRAY: &str = "hide-tray"; + pub const OPTION_ONE_WAY_CLIPBOARD_REDIRECTION: &str = "one-way-clipboard-redirection"; + pub const OPTION_ENABLE_CLIPBOARD_INIT_SYNC: &str = "enable-clipboard-init-sync"; + pub const OPTION_ALLOW_LOGON_SCREEN_PASSWORD: &str = "allow-logon-screen-password"; + pub const OPTION_ONE_WAY_FILE_TRANSFER: &str = "one-way-file-transfer"; // flutter local options pub const OPTION_FLUTTER_REMOTE_MENUBAR_STATE: &str = "remoteMenubarState"; @@ -2362,6 +2366,10 @@ pub mod keys { OPTION_HIDE_HELP_CARDS, OPTION_DEFAULT_CONNECT_PASSWORD, OPTION_HIDE_TRAY, + OPTION_ONE_WAY_CLIPBOARD_REDIRECTION, + OPTION_ENABLE_CLIPBOARD_INIT_SYNC, + OPTION_ALLOW_LOGON_SCREEN_PASSWORD, + OPTION_ONE_WAY_FILE_TRANSFER, ]; } diff --git a/libs/hbb_common/src/platform/mod.rs b/libs/hbb_common/src/platform/mod.rs index 5dc004a81b7f..d01333558a64 100644 --- a/libs/hbb_common/src/platform/mod.rs +++ b/libs/hbb_common/src/platform/mod.rs @@ -79,3 +79,9 @@ where libc::signal(libc::SIGSEGV, breakdown_signal_handler as _); } } + +#[cfg(any(target_os = "android", target_os = "ios"))] +#[inline] +fn is_prelogin() -> bool { + false +} diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index 21a5af9a3599..46eb6f546130 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -26,7 +26,7 @@ use crossbeam_queue::ArrayQueue; use hbb_common::tokio::sync::mpsc::error::TryRecvError; use hbb_common::{ allow_err, - config::{PeerConfig, TransferSerde}, + config::{self, PeerConfig, TransferSerde}, fs::{ self, can_enable_overwrite_detection, get_job, get_string, new_send_confirm, DigestCheckResult, RemoveJobMeta, @@ -1201,13 +1201,18 @@ impl Remote { &peer_platform, crate::clipboard::ClipboardSide::Client, ) { - let sender = self.sender.clone(); - let permission_config = self.handler.get_permission_config(); - tokio::spawn(async move { - if permission_config.is_text_clipboard_required() { - sender.send(Data::Message(msg_out)).ok(); - } - }); + if crate::get_builtin_option( + config::keys::OPTION_ENABLE_CLIPBOARD_INIT_SYNC, + ) != "N" + { + let sender = self.sender.clone(); + let permission_config = self.handler.get_permission_config(); + tokio::spawn(async move { + if permission_config.is_text_clipboard_required() { + sender.send(Data::Message(msg_out)).ok(); + } + }); + } } // on connection established client @@ -1618,7 +1623,7 @@ impl Remote { }, Some(message::Union::MessageBox(msgbox)) => { let mut link = msgbox.link; - if let Some(v) = hbb_common::config::HELPER_URL.get(&link as &str) { + if let Some(v) = config::HELPER_URL.get(&link as &str) { link = v.to_string(); } else { log::warn!("Message box ignore link {} for security", &link); diff --git a/src/common.rs b/src/common.rs index 2e801e66d2f8..9950850708f5 100644 --- a/src/common.rs +++ b/src/common.rs @@ -1650,3 +1650,13 @@ mod tests { ); } } + +#[inline] +pub fn get_builtin_option(key: &str) -> String { + config::BUILTIN_SETTINGS + .read() + .unwrap() + .get(key) + .cloned() + .unwrap_or_default() +} diff --git a/src/server/connection.rs b/src/server/connection.rs index e351b0d5006b..2863c2365db3 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -27,7 +27,7 @@ use hbb_common::platform::linux::run_cmds; #[cfg(target_os = "android")] use hbb_common::protobuf::EnumOrUnknown; use hbb_common::{ - config::{self, Config, TrustedDevice}, + config::{self, keys, Config, TrustedDevice}, fs::{self, can_enable_overwrite_detection}, futures::{SinkExt, StreamExt}, get_time, get_version_number, @@ -335,7 +335,7 @@ impl Connection { clipboard: Connection::permission("enable-clipboard"), audio: Connection::permission("enable-audio"), // to-do: make sure is the option correct here - file: Connection::permission(config::keys::OPTION_ENABLE_FILE_TRANSFER), + file: Connection::permission(keys::OPTION_ENABLE_FILE_TRANSFER), restart: Connection::permission("enable-remote-restart"), recording: Connection::permission("enable-record-session"), block_input: Connection::permission("enable-block-input"), @@ -1359,7 +1359,10 @@ impl Connection { if !self.follow_remote_window { noperms.push(NAME_WINDOW_FOCUS); } - if !self.clipboard_enabled() || !self.peer_keyboard_enabled() { + if !self.clipboard_enabled() + || !self.peer_keyboard_enabled() + || crate::get_builtin_option(keys::OPTION_ONE_WAY_CLIPBOARD_REDIRECTION) == "Y" + { noperms.push(super::clipboard_service::NAME); } if !self.audio_enabled() { @@ -1621,8 +1624,8 @@ impl Connection { #[inline] fn enable_trusted_devices() -> bool { config::option2bool( - config::keys::OPTION_ENABLE_TRUSTED_DEVICES, - &Config::get_option(config::keys::OPTION_ENABLE_TRUSTED_DEVICES), + keys::OPTION_ENABLE_TRUSTED_DEVICES, + &Config::get_option(keys::OPTION_ENABLE_TRUSTED_DEVICES), ) } @@ -1689,7 +1692,7 @@ impl Connection { } match lr.union { Some(login_request::Union::FileTransfer(ft)) => { - if !Connection::permission(config::keys::OPTION_ENABLE_FILE_TRANSFER) { + if !Connection::permission(keys::OPTION_ENABLE_FILE_TRANSFER) { self.send_login_error("No permission of file transfer") .await; sleep(1.).await; @@ -1762,7 +1765,9 @@ impl Connection { self.send_login_error(crate::client::LOGIN_MSG_OFFLINE) .await; return false; - } else if password::approve_mode() == ApproveMode::Click + } else if (password::approve_mode() == ApproveMode::Click + && !(crate::platform::is_prelogin() + && crate::get_builtin_option(keys::OPTION_ALLOW_LOGON_SCREEN_PASSWORD) == "Y")) || password::approve_mode() == ApproveMode::Both && !password::has_valid_password() { self.try_start_cm(lr.my_id, lr.my_name, false); @@ -2133,6 +2138,24 @@ impl Connection { } return true; } + if crate::get_builtin_option(keys::OPTION_ONE_WAY_FILE_TRANSFER) == "Y" { + match fa.union { + Some(file_action::Union::Send(_)) + | Some(file_action::Union::RemoveFile(_)) + | Some(file_action::Union::Rename(_)) + | Some(file_action::Union::Create(_)) + | Some(file_action::Union::RemoveDir(_)) => { + self.send(fs::new_error( + 0, + "One-way file transfer is enabled on controlled side", + 0, + )) + .await; + return true; + } + _ => {} + } + } match fa.union { Some(file_action::Union::ReadDir(rd)) => { self.read_dir(&rd.path, rd.include_hidden); diff --git a/src/ui_cm_interface.rs b/src/ui_cm_interface.rs index 49f91a9dab30..e75683d8a30a 100644 --- a/src/ui_cm_interface.rs +++ b/src/ui_cm_interface.rs @@ -565,7 +565,7 @@ impl IpcTaskRunner { log::debug!( "Process clipboard message from clip, stop: {}, is_stopping_allowed: {}, is_clipboard_enabled: {}, file_transfer_enabled: {}, file_transfer_enabled_peer: {}", stop, is_stopping_allowed, is_clipboard_enabled, file_transfer_enabled, file_transfer_enabled_peer); - if stop { + if stop || crate::get_builtin_option(OPTION_ONE_WAY_FILE_TRANSFER) == "Y"{ ContextSend::set_is_stopped(); } else { allow_err!(self.tx.send(Data::ClipboardFile(_clip))); diff --git a/src/ui_interface.rs b/src/ui_interface.rs index ad2db6d01dbf..e9f2875be5dd 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -207,12 +207,7 @@ pub fn get_hard_option(key: String) -> String { #[inline] pub fn get_builtin_option(key: &str) -> String { - config::BUILTIN_SETTINGS - .read() - .unwrap() - .get(key) - .cloned() - .unwrap_or_default() + crate::get_builtin_option(key) } #[inline] From e1a6ccc10053c4c1914893a0d689a712d8891cae Mon Sep 17 00:00:00 2001 From: rustdesk Date: Wed, 18 Sep 2024 12:37:26 +0800 Subject: [PATCH 201/541] fix ci --- libs/hbb_common/src/platform/mod.rs | 6 ------ src/platform/mod.rs | 6 ++++++ 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/libs/hbb_common/src/platform/mod.rs b/libs/hbb_common/src/platform/mod.rs index d01333558a64..5dc004a81b7f 100644 --- a/libs/hbb_common/src/platform/mod.rs +++ b/libs/hbb_common/src/platform/mod.rs @@ -79,9 +79,3 @@ where libc::signal(libc::SIGSEGV, breakdown_signal_handler as _); } } - -#[cfg(any(target_os = "android", target_os = "ios"))] -#[inline] -fn is_prelogin() -> bool { - false -} diff --git a/src/platform/mod.rs b/src/platform/mod.rs index 7bb503fdd5c5..7b4f9c78e675 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -128,6 +128,12 @@ impl Drop for InstallingService { } } +#[cfg(any(target_os = "android", target_os = "ios"))] +#[inline] +fn is_prelogin() -> bool { + false +} + #[cfg(test)] mod tests { use super::*; From e20f5dd001f3e7fbc9e932ea227cdb8e94d80c15 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Wed, 18 Sep 2024 13:00:15 +0800 Subject: [PATCH 202/541] fix ci --- src/platform/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/platform/mod.rs b/src/platform/mod.rs index 7b4f9c78e675..169bdb199f45 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -130,7 +130,7 @@ impl Drop for InstallingService { #[cfg(any(target_os = "android", target_os = "ios"))] #[inline] -fn is_prelogin() -> bool { +pub fn is_prelogin() -> bool { false } From e5ec6957fe86fe83902e6745d30b0b321b38e190 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Wed, 18 Sep 2024 18:22:12 +0800 Subject: [PATCH 203/541] fix: option OPTION_ONE_WAY_FILE_TRANSFER (#9387) Signed-off-by: fufesou --- libs/clipboard/src/lib.rs | 2 +- src/client/io_loop.rs | 2 +- src/ui_cm_interface.rs | 11 ++++++++--- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/libs/clipboard/src/lib.rs b/libs/clipboard/src/lib.rs index a5da25512d2a..30055740ed8c 100644 --- a/libs/clipboard/src/lib.rs +++ b/libs/clipboard/src/lib.rs @@ -132,7 +132,7 @@ impl ClipboardFile { ) } - pub fn is_stopping_allowed_from_peer(&self) -> bool { + pub fn is_beginning_message(&self) -> bool { matches!( self, ClipboardFile::MonitorReady | ClipboardFile::FormatList { .. } diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index 46eb6f546130..c23c967b5809 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -1895,7 +1895,7 @@ impl Remote { return; }; - let is_stopping_allowed = clip.is_stopping_allowed_from_peer(); + let is_stopping_allowed = clip.is_beginning_message(); let file_transfer_enabled = self.handler.lc.read().unwrap().enable_file_copy_paste.v; let stop = is_stopping_allowed && !file_transfer_enabled; log::debug!( diff --git a/src/ui_cm_interface.rs b/src/ui_cm_interface.rs index e75683d8a30a..549798a51728 100644 --- a/src/ui_cm_interface.rs +++ b/src/ui_cm_interface.rs @@ -440,7 +440,7 @@ impl IpcTaskRunner { Data::ClipboardFile(_clip) => { #[cfg(any(target_os = "windows", target_os="linux", target_os = "macos"))] { - let is_stopping_allowed = _clip.is_stopping_allowed_from_peer(); + let is_stopping_allowed = _clip.is_beginning_message(); let is_clipboard_enabled = ContextSend::is_enabled(); let file_transfer_enabled = self.file_transfer_enabled; let stop = !is_stopping_allowed && !(is_clipboard_enabled && file_transfer_enabled); @@ -565,10 +565,15 @@ impl IpcTaskRunner { log::debug!( "Process clipboard message from clip, stop: {}, is_stopping_allowed: {}, is_clipboard_enabled: {}, file_transfer_enabled: {}, file_transfer_enabled_peer: {}", stop, is_stopping_allowed, is_clipboard_enabled, file_transfer_enabled, file_transfer_enabled_peer); - if stop || crate::get_builtin_option(OPTION_ONE_WAY_FILE_TRANSFER) == "Y"{ + if stop { ContextSend::set_is_stopped(); } else { - allow_err!(self.tx.send(Data::ClipboardFile(_clip))); + if _clip.is_beginning_message() && crate::get_builtin_option(OPTION_ONE_WAY_FILE_TRANSFER) == "Y" { + // If one way file transfer is enabled, don't send clipboard file to client + // Don't call `ContextSend::set_is_stopped()`, because it will stop bidirectional file copy&paste. + } else { + allow_err!(self.tx.send(Data::ClipboardFile(_clip))); + } } } } From d08c335fdf341b335419baf45b941aaccd73a805 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Wed, 18 Sep 2024 19:29:35 +0800 Subject: [PATCH 204/541] fix: file transfer, show error, msgbox (#9389) * fix: file transfer, show error, msgbox Signed-off-by: fufesou * fix: translation Signed-off-by: fufesou --------- Signed-off-by: fufesou --- flutter/lib/models/file_model.dart | 21 +++++++++++++++-- src/lang/ar.rs | 1 + src/lang/be.rs | 1 + src/lang/bg.rs | 1 + src/lang/ca.rs | 1 + src/lang/cn.rs | 1 + src/lang/cs.rs | 1 + src/lang/da.rs | 1 + src/lang/de.rs | 1 + src/lang/el.rs | 1 + src/lang/en.rs | 1 + src/lang/eo.rs | 1 + src/lang/es.rs | 1 + src/lang/et.rs | 1 + src/lang/eu.rs | 1 + src/lang/fa.rs | 1 + src/lang/fr.rs | 1 + src/lang/he.rs | 1 + src/lang/hr.rs | 1 + src/lang/hu.rs | 1 + src/lang/id.rs | 1 + src/lang/it.rs | 1 + src/lang/ja.rs | 1 + src/lang/ko.rs | 1 + src/lang/kz.rs | 1 + src/lang/lt.rs | 1 + src/lang/lv.rs | 1 + src/lang/nb.rs | 1 + src/lang/nl.rs | 1 + src/lang/pl.rs | 1 + src/lang/pt_PT.rs | 1 + src/lang/ptbr.rs | 1 + src/lang/ro.rs | 1 + src/lang/ru.rs | 1 + src/lang/sk.rs | 1 + src/lang/sl.rs | 1 + src/lang/sq.rs | 1 + src/lang/sr.rs | 1 + src/lang/sv.rs | 1 + src/lang/template.rs | 1 + src/lang/th.rs | 1 + src/lang/tr.rs | 1 + src/lang/tw.rs | 1 + src/lang/uk.rs | 1 + src/lang/vn.rs | 1 + src/server/connection.rs | 38 ++++++++++++++++++++---------- 46 files changed, 88 insertions(+), 15 deletions(-) diff --git a/flutter/lib/models/file_model.dart b/flutter/lib/models/file_model.dart index 68af293804ed..a0d5bc0b5588 100644 --- a/flutter/lib/models/file_model.dart +++ b/flutter/lib/models/file_model.dart @@ -34,6 +34,7 @@ class JobID { } typedef GetSessionID = SessionID Function(); +typedef GetDialogManager = OverlayDialogManager? Function(); class FileModel { final WeakReference parent; @@ -45,13 +46,15 @@ class FileModel { late final FileController remoteController; late final GetSessionID getSessionID; + late final GetDialogManager getDialogManager; SessionID get sessionId => getSessionID(); late final FileDialogEventLoop evtLoop; FileModel(this.parent) { getSessionID = () => parent.target!.sessionId; + getDialogManager = () => parent.target?.dialogManager; fileFetcher = FileFetcher(getSessionID); - jobController = JobController(getSessionID); + jobController = JobController(getSessionID, getDialogManager); localController = FileController( isLocal: true, getSessionID: getSessionID, @@ -736,14 +739,19 @@ class FileController { } } +const _kOneWayFileTransferError = 'one-way-file-transfer-tip'; + class JobController { static final JobID jobID = JobID(); final jobTable = List.empty(growable: true).obs; final jobResultListener = JobResultListener>(); final GetSessionID getSessionID; + final GetDialogManager getDialogManager; SessionID get sessionId => getSessionID(); + OverlayDialogManager? get alogManager => getDialogManager(); + int _lastTimeShowMsgbox = DateTime.now().millisecondsSinceEpoch; - JobController(this.getSessionID); + JobController(this.getSessionID, this.getDialogManager); int getJob(int id) { return jobTable.indexWhere((element) => element.id == id); @@ -882,6 +890,15 @@ class JobController { } jobTable.refresh(); } + if (err == _kOneWayFileTransferError) { + if (DateTime.now().millisecondsSinceEpoch - _lastTimeShowMsgbox > 3000) { + final dm = alogManager; + if (dm != null) { + _lastTimeShowMsgbox = DateTime.now().millisecondsSinceEpoch; + msgBox(sessionId, 'custom-nocancel', 'Error', err, '', dm); + } + } + } debugPrint("jobError $evt"); } diff --git a/src/lang/ar.rs b/src/lang/ar.rs index 68c041481e5f..a157d3d9d825 100644 --- a/src/lang/ar.rs +++ b/src/lang/ar.rs @@ -644,5 +644,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Parent directory", ""), ("Resume", ""), ("Invalid file name", ""), + ("one-way-file-transfer-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/be.rs b/src/lang/be.rs index 136ccf9feec5..4fbfff131c17 100644 --- a/src/lang/be.rs +++ b/src/lang/be.rs @@ -644,5 +644,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Parent directory", ""), ("Resume", ""), ("Invalid file name", ""), + ("one-way-file-transfer-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/bg.rs b/src/lang/bg.rs index 7efdd0dfa749..2992911a814c 100644 --- a/src/lang/bg.rs +++ b/src/lang/bg.rs @@ -644,5 +644,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Parent directory", ""), ("Resume", "Възобновяване"), ("Invalid file name", "Невалидно име за файл"), + ("one-way-file-transfer-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ca.rs b/src/lang/ca.rs index 0aa64ec28e69..260b0b4c6bf0 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -644,5 +644,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Parent directory", ""), ("Resume", ""), ("Invalid file name", ""), + ("one-way-file-transfer-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 62cb5452c601..dc986aaca8d2 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -644,5 +644,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Parent directory", "父目录"), ("Resume", "继续"), ("Invalid file name", "无效文件名"), + ("one-way-file-transfer-tip", "被控端启用了单项文件传输"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index 8ff3d8069187..c3a1b9758773 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -644,5 +644,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Parent directory", "Rodičovský adresář"), ("Resume", "Pokračovat"), ("Invalid file name", "Nesprávný název souboru"), + ("one-way-file-transfer-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index 03cf47d4bb8d..be02a7360ac8 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -644,5 +644,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Parent directory", "mappe"), ("Resume", "Fortsæt"), ("Invalid file name", "Ugyldigt filnavn"), + ("one-way-file-transfer-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index e8e61543a48e..3069ebe3c957 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -644,5 +644,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Parent directory", "Übergeordnetes Verzeichnis"), ("Resume", "Fortsetzen"), ("Invalid file name", "Ungültiger Dateiname"), + ("one-way-file-transfer-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/el.rs b/src/lang/el.rs index b3ce7dcaf80c..e05aa9fb89e7 100644 --- a/src/lang/el.rs +++ b/src/lang/el.rs @@ -644,5 +644,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Parent directory", ""), ("Resume", ""), ("Invalid file name", ""), + ("one-way-file-transfer-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/en.rs b/src/lang/en.rs index 917422d0dc4f..55ef4470c55b 100644 --- a/src/lang/en.rs +++ b/src/lang/en.rs @@ -234,5 +234,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About RustDesk", ""), ("network_error_tip", "Please check your network connection, then click retry."), ("enable-trusted-devices-tip", "Skip 2FA verification on trusted devices"), + ("one-way-file-transfer-tip", "One-way file transfer is enabled on the controlled side."), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 144bf7bc3c45..d8cb8ec34040 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -644,5 +644,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Parent directory", ""), ("Resume", ""), ("Invalid file name", ""), + ("one-way-file-transfer-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index 794c751c00f3..c087b287ca4e 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -644,5 +644,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Parent directory", "Directorio superior"), ("Resume", "Continuar"), ("Invalid file name", "Nombre de archivo no válido"), + ("one-way-file-transfer-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/et.rs b/src/lang/et.rs index cc3f3afc3922..24901265f5c4 100644 --- a/src/lang/et.rs +++ b/src/lang/et.rs @@ -644,5 +644,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Parent directory", ""), ("Resume", ""), ("Invalid file name", ""), + ("one-way-file-transfer-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eu.rs b/src/lang/eu.rs index 412b4e74060c..9213c9689ebe 100644 --- a/src/lang/eu.rs +++ b/src/lang/eu.rs @@ -644,5 +644,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Parent directory", ""), ("Resume", ""), ("Invalid file name", ""), + ("one-way-file-transfer-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index b6949aa50436..47988a95d929 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -644,5 +644,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Parent directory", ""), ("Resume", ""), ("Invalid file name", ""), + ("one-way-file-transfer-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 31d78640888d..688bcf25f7cb 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -644,5 +644,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Parent directory", ""), ("Resume", ""), ("Invalid file name", ""), + ("one-way-file-transfer-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/he.rs b/src/lang/he.rs index 07d9aa977d17..7fc4758e1490 100644 --- a/src/lang/he.rs +++ b/src/lang/he.rs @@ -644,5 +644,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Parent directory", ""), ("Resume", ""), ("Invalid file name", ""), + ("one-way-file-transfer-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hr.rs b/src/lang/hr.rs index 1dca1c7e0c05..4671a0d47f97 100644 --- a/src/lang/hr.rs +++ b/src/lang/hr.rs @@ -644,5 +644,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Parent directory", ""), ("Resume", ""), ("Invalid file name", ""), + ("one-way-file-transfer-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index be089347aca7..2ceb6bce4ce9 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -644,5 +644,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Parent directory", ""), ("Resume", ""), ("Invalid file name", ""), + ("one-way-file-transfer-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index 42c36b5c744e..168c78117f78 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -644,5 +644,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Parent directory", ""), ("Resume", ""), ("Invalid file name", ""), + ("one-way-file-transfer-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index 7c18b52d2a14..08c00ee1ffbf 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -644,5 +644,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Parent directory", "Cartella principale"), ("Resume", "Riprendi"), ("Invalid file name", "Nome file non valido"), + ("one-way-file-transfer-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 9d0f6ea05117..e58ff25f78ad 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -644,5 +644,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Parent directory", "親ディレクトリ"), ("Resume", "再開"), ("Invalid file name", "無効なファイル名"), + ("one-way-file-transfer-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 3e84ca262f54..daf85e632245 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -644,5 +644,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Parent directory", ""), ("Resume", ""), ("Invalid file name", ""), + ("one-way-file-transfer-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index c47764c8190f..85829e84a276 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -644,5 +644,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Parent directory", ""), ("Resume", ""), ("Invalid file name", ""), + ("one-way-file-transfer-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lt.rs b/src/lang/lt.rs index b1d0317f89d0..7387d34e3235 100644 --- a/src/lang/lt.rs +++ b/src/lang/lt.rs @@ -644,5 +644,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Parent directory", ""), ("Resume", ""), ("Invalid file name", ""), + ("one-way-file-transfer-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lv.rs b/src/lang/lv.rs index bb1090dca9c2..0d8b193876b1 100644 --- a/src/lang/lv.rs +++ b/src/lang/lv.rs @@ -644,5 +644,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Parent directory", "Vecākdirektorijs"), ("Resume", "Atsākt"), ("Invalid file name", "Nederīgs faila nosaukums"), + ("one-way-file-transfer-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nb.rs b/src/lang/nb.rs index d23191ef2b73..f3c4ed410d6a 100644 --- a/src/lang/nb.rs +++ b/src/lang/nb.rs @@ -644,5 +644,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Parent directory", ""), ("Resume", ""), ("Invalid file name", ""), + ("one-way-file-transfer-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nl.rs b/src/lang/nl.rs index 78b36b2e33b5..c6d7bcbd3614 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -644,5 +644,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Parent directory", "Hoofdmap"), ("Resume", "Hervatten"), ("Invalid file name", "Ongeldige bestandsnaam"), + ("one-way-file-transfer-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 725d67996a6b..59e4cbad2a11 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -644,5 +644,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Parent directory", "Folder nadrzędny"), ("Resume", "Wznów"), ("Invalid file name", "Nieprawidłowa nazwa pliku"), + ("one-way-file-transfer-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index 6afa4c528eb4..d172cb4f9115 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -644,5 +644,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Parent directory", ""), ("Resume", ""), ("Invalid file name", ""), + ("one-way-file-transfer-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index a5ba6de7b0fa..6a5300028f4a 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -644,5 +644,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Parent directory", "Diretório pai"), ("Resume", "Continuar"), ("Invalid file name", "Nome de arquivo inválido"), + ("one-way-file-transfer-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ro.rs b/src/lang/ro.rs index 0f11e5449534..7b3ba2b2881b 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -644,5 +644,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Parent directory", ""), ("Resume", ""), ("Invalid file name", ""), + ("one-way-file-transfer-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index b13c4a1b6634..59a49959584b 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -644,5 +644,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Parent directory", "Родительская директория"), ("Resume", "Продолжить"), ("Invalid file name", "Неверное имя файла"), + ("one-way-file-transfer-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 51d090328349..7b4678423714 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -644,5 +644,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Parent directory", "Rodičovský adresár"), ("Resume", "Obnoviť"), ("Invalid file name", "Nesprávny názov súboru"), + ("one-way-file-transfer-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sl.rs b/src/lang/sl.rs index 7c8d97494867..637859cd4017 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -644,5 +644,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Parent directory", ""), ("Resume", ""), ("Invalid file name", ""), + ("one-way-file-transfer-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sq.rs b/src/lang/sq.rs index 0a73e021744d..fe9a30c0cc7e 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -644,5 +644,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Parent directory", ""), ("Resume", ""), ("Invalid file name", ""), + ("one-way-file-transfer-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sr.rs b/src/lang/sr.rs index 4d3654ea9987..906dc0d3f2a2 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -644,5 +644,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Parent directory", ""), ("Resume", ""), ("Invalid file name", ""), + ("one-way-file-transfer-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sv.rs b/src/lang/sv.rs index 9d7956545bcf..e877aca0cde2 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -644,5 +644,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Parent directory", ""), ("Resume", ""), ("Invalid file name", ""), + ("one-way-file-transfer-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index 76e491c91ce7..7b08bf3a3bd4 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -644,5 +644,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Parent directory", ""), ("Resume", ""), ("Invalid file name", ""), + ("one-way-file-transfer-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/th.rs b/src/lang/th.rs index acd14c8f9c56..1b818f479e77 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -644,5 +644,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Parent directory", ""), ("Resume", ""), ("Invalid file name", ""), + ("one-way-file-transfer-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index f926e94549a2..26085f539319 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -644,5 +644,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Parent directory", ""), ("Resume", ""), ("Invalid file name", ""), + ("one-way-file-transfer-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 9bbef24311d6..690792dbb5f3 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -644,5 +644,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Parent directory", "父目錄"), ("Resume", "繼續"), ("Invalid file name", "無效文件名"), + ("one-way-file-transfer-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/uk.rs b/src/lang/uk.rs index 6a177059cfa2..ccc73b7b049b 100644 --- a/src/lang/uk.rs +++ b/src/lang/uk.rs @@ -644,5 +644,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Parent directory", ""), ("Resume", ""), ("Invalid file name", ""), + ("one-way-file-transfer-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index 88f70a8e2c4d..408060a09c3c 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -644,5 +644,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Parent directory", ""), ("Resume", ""), ("Invalid file name", ""), + ("one-way-file-transfer-tip", ""), ].iter().cloned().collect(); } diff --git a/src/server/connection.rs b/src/server/connection.rs index 2863c2365db3..dbe8b9614317 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -2139,22 +2139,34 @@ impl Connection { return true; } if crate::get_builtin_option(keys::OPTION_ONE_WAY_FILE_TRANSFER) == "Y" { - match fa.union { - Some(file_action::Union::Send(_)) - | Some(file_action::Union::RemoveFile(_)) - | Some(file_action::Union::Rename(_)) - | Some(file_action::Union::Create(_)) - | Some(file_action::Union::RemoveDir(_)) => { - self.send(fs::new_error( - 0, - "One-way file transfer is enabled on controlled side", - 0, - )) - .await; - return true; + let mut job_id = None; + match &fa.union { + Some(file_action::Union::Send(s)) => { + job_id = Some(s.id); + } + Some(file_action::Union::RemoveFile(rf)) => { + job_id = Some(rf.id); + } + Some(file_action::Union::Rename(r)) => { + job_id = Some(r.id); + } + Some(file_action::Union::Create(c)) => { + job_id = Some(c.id); + } + Some(file_action::Union::RemoveDir(rd)) => { + job_id = Some(rd.id); } _ => {} } + if let Some(job_id) = job_id { + self.send(fs::new_error( + job_id, + "one-way-file-transfer-tip", + 0, + )) + .await; + return true; + } } match fa.union { Some(file_action::Union::ReadDir(rd)) => { From 88a99211f31e07c6fb1ff6a3fadb73d2aa421569 Mon Sep 17 00:00:00 2001 From: 21pages Date: Thu, 19 Sep 2024 18:47:37 +0800 Subject: [PATCH 205/541] replace pkexec with gtk sudo (#9383) * Fix https://github.com/rustdesk/rustdesk/issues/9286, replace pkexec with gtk sudo. Tested on gnome (ubuntu 22.04, debian 13), xfce (manjaro, suse), kde (kubuntu 23), lxqt (lubuntu 22), Cinnamon (mint 21.3), Mate (mint 21.2) * Fix incorrect config of the main window opened by the tray, replace xdg-open with run_me, replace with dbus + run_me * Fix `check_if_stop_service`, it causes the problem fixed in https://github.com/rustdesk/rustdesk/pull/8414, now revert that fix and fix itself. Signed-off-by: 21pages --- Cargo.lock | 23 +- Cargo.toml | 3 + build.py | 4 - res/com.rustdesk.RustDesk.policy | 23 - src/core_main.rs | 7 + src/lang/ar.rs | 2 + src/lang/be.rs | 2 + src/lang/bg.rs | 2 + src/lang/ca.rs | 2 + src/lang/cn.rs | 2 + src/lang/cs.rs | 2 + src/lang/da.rs | 2 + src/lang/de.rs | 2 + src/lang/el.rs | 2 + src/lang/eo.rs | 2 + src/lang/es.rs | 2 + src/lang/et.rs | 2 + src/lang/eu.rs | 2 + src/lang/fa.rs | 2 + src/lang/fr.rs | 2 + src/lang/he.rs | 2 + src/lang/hr.rs | 2 + src/lang/hu.rs | 2 + src/lang/id.rs | 2 + src/lang/it.rs | 2 + src/lang/ja.rs | 2 + src/lang/ko.rs | 2 + src/lang/kz.rs | 2 + src/lang/lt.rs | 2 + src/lang/lv.rs | 2 + src/lang/nb.rs | 2 + src/lang/nl.rs | 2 + src/lang/pl.rs | 2 + src/lang/pt_PT.rs | 2 + src/lang/ptbr.rs | 2 + src/lang/ro.rs | 2 + src/lang/ru.rs | 2 + src/lang/sk.rs | 2 + src/lang/sl.rs | 2 + src/lang/sq.rs | 2 + src/lang/sr.rs | 2 + src/lang/sv.rs | 2 + src/lang/template.rs | 2 + src/lang/th.rs | 2 + src/lang/tr.rs | 2 + src/lang/tw.rs | 2 + src/lang/uk.rs | 2 + src/lang/vn.rs | 2 + src/platform/gtk_sudo.rs | 774 +++++++++++++++++++++++++++++++ src/platform/linux.rs | 58 +-- src/platform/mod.rs | 3 + src/tray.rs | 9 +- 52 files changed, 916 insertions(+), 74 deletions(-) delete mode 100644 res/com.rustdesk.RustDesk.policy create mode 100644 src/platform/gtk_sudo.rs diff --git a/Cargo.lock b/Cargo.lock index 3e793ddb791d..486b7a6f9be1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -860,6 +860,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "chrono" version = "0.4.38" @@ -3967,11 +3973,23 @@ checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" dependencies = [ "bitflags 2.6.0", "cfg-if 1.0.0", - "cfg_aliases", + "cfg_aliases 0.1.1", "libc", "memoffset 0.9.1", ] +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags 2.6.0", + "cfg-if 1.0.0", + "cfg_aliases 0.2.1", + "libc", +] + [[package]] name = "nom" version = "7.1.3" @@ -5494,6 +5512,7 @@ dependencies = [ "flutter_rust_bridge", "fon", "fruitbasket", + "gtk", "hbb_common", "hex", "hound", @@ -5508,6 +5527,7 @@ dependencies = [ "libpulse-simple-binding", "mac_address", "magnum-opus", + "nix 0.29.0", "num_cpus", "objc", "objc_id", @@ -5539,6 +5559,7 @@ dependencies = [ "system_shutdown", "tao", "tauri-winrt-notification", + "termios", "totp-rs", "tray-icon", "url", diff --git a/Cargo.toml b/Cargo.toml index 28c2c363d46e..c71b2918bd27 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -161,6 +161,9 @@ x11-clipboard = {git="https://github.com/clslaid/x11-clipboard", branch = "feat/ x11rb = {version = "0.12", features = ["all-extensions"], optional = true} percent-encoding = {version = "2.3", 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] android_logger = "0.13" diff --git a/build.py b/build.py index be13207ffeab..3ad206ab1898 100755 --- a/build.py +++ b/build.py @@ -331,8 +331,6 @@ def build_flutter_deb(version, features): 'cp ../res/rustdesk.desktop tmpdeb/usr/share/applications/rustdesk.desktop') system2( '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( 'cp ../res/startwm.sh tmpdeb/etc/rustdesk/') system2( @@ -376,8 +374,6 @@ def build_deb_from_folder(version, binary_folder): 'cp ../res/rustdesk.desktop tmpdeb/usr/share/applications/rustdesk.desktop') system2( '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( "echo \"#!/bin/sh\" >> tmpdeb/usr/share/rustdesk/files/polkit && chmod a+x tmpdeb/usr/share/rustdesk/files/polkit") diff --git a/res/com.rustdesk.RustDesk.policy b/res/com.rustdesk.RustDesk.policy deleted file mode 100644 index 55f13629b7fa..000000000000 --- a/res/com.rustdesk.RustDesk.policy +++ /dev/null @@ -1,23 +0,0 @@ - - - - RustDesk - https://rustdesk.com/ - rustdesk - - Change RustDesk options - Authentication is required to change RustDesk options - 要更改RustDesk选项, 需要您先通过身份验证 - 要變更RustDesk選項, 需要您先通過身份驗證 - Authentifizierung zum Ändern der RustDesk-Optionen - /usr/share/rustdesk/files/polkit - true - - auth_admin - auth_admin - auth_admin - - - diff --git a/src/core_main.rs b/src/core_main.rs index 5d137516ee4b..23d7706d4738 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -482,6 +482,13 @@ pub fn core_main() -> Option> { crate::flutter::connection_manager::start_cm_no_ui(); } return None; + } else if args[0] == "-gtk-sudo" { + // rustdesk service kill `rustdesk --` processes + #[cfg(target_os = "linux")] + if args.len() > 2 { + crate::platform::gtk_sudo::exec(); + } + return None; } else { #[cfg(all(feature = "flutter", feature = "plugin_framework"))] #[cfg(not(any(target_os = "android", target_os = "ios")))] diff --git a/src/lang/ar.rs b/src/lang/ar.rs index a157d3d9d825..be1a6b7675aa 100644 --- a/src/lang/ar.rs +++ b/src/lang/ar.rs @@ -645,5 +645,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Resume", ""), ("Invalid file name", ""), ("one-way-file-transfer-tip", ""), + ("Authentication Required", ""), + ("Authenticate", ""), ].iter().cloned().collect(); } diff --git a/src/lang/be.rs b/src/lang/be.rs index 4fbfff131c17..fb8444becb5e 100644 --- a/src/lang/be.rs +++ b/src/lang/be.rs @@ -645,5 +645,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Resume", ""), ("Invalid file name", ""), ("one-way-file-transfer-tip", ""), + ("Authentication Required", ""), + ("Authenticate", ""), ].iter().cloned().collect(); } diff --git a/src/lang/bg.rs b/src/lang/bg.rs index 2992911a814c..b683a5293c45 100644 --- a/src/lang/bg.rs +++ b/src/lang/bg.rs @@ -645,5 +645,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Resume", "Възобновяване"), ("Invalid file name", "Невалидно име за файл"), ("one-way-file-transfer-tip", ""), + ("Authentication Required", ""), + ("Authenticate", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ca.rs b/src/lang/ca.rs index 260b0b4c6bf0..a52da9735fdb 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -645,5 +645,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Resume", ""), ("Invalid file name", ""), ("one-way-file-transfer-tip", ""), + ("Authentication Required", ""), + ("Authenticate", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index dc986aaca8d2..887a7297114b 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -645,5 +645,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Resume", "继续"), ("Invalid file name", "无效文件名"), ("one-way-file-transfer-tip", "被控端启用了单项文件传输"), + ("Authentication Required", "需要身份验证"), + ("Authenticate", "认证"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index c3a1b9758773..67588bfb8486 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -645,5 +645,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Resume", "Pokračovat"), ("Invalid file name", "Nesprávný název souboru"), ("one-way-file-transfer-tip", ""), + ("Authentication Required", ""), + ("Authenticate", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index be02a7360ac8..aea1514ae936 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -645,5 +645,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Resume", "Fortsæt"), ("Invalid file name", "Ugyldigt filnavn"), ("one-way-file-transfer-tip", ""), + ("Authentication Required", ""), + ("Authenticate", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index 3069ebe3c957..38720f53797c 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -645,5 +645,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Resume", "Fortsetzen"), ("Invalid file name", "Ungültiger Dateiname"), ("one-way-file-transfer-tip", ""), + ("Authentication Required", ""), + ("Authenticate", ""), ].iter().cloned().collect(); } diff --git a/src/lang/el.rs b/src/lang/el.rs index e05aa9fb89e7..fecff28945fe 100644 --- a/src/lang/el.rs +++ b/src/lang/el.rs @@ -645,5 +645,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Resume", ""), ("Invalid file name", ""), ("one-way-file-transfer-tip", ""), + ("Authentication Required", ""), + ("Authenticate", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index d8cb8ec34040..eb512922ed3c 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -645,5 +645,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Resume", ""), ("Invalid file name", ""), ("one-way-file-transfer-tip", ""), + ("Authentication Required", ""), + ("Authenticate", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index c087b287ca4e..e42a5abed84e 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -645,5 +645,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Resume", "Continuar"), ("Invalid file name", "Nombre de archivo no válido"), ("one-way-file-transfer-tip", ""), + ("Authentication Required", ""), + ("Authenticate", ""), ].iter().cloned().collect(); } diff --git a/src/lang/et.rs b/src/lang/et.rs index 24901265f5c4..2443faae9b01 100644 --- a/src/lang/et.rs +++ b/src/lang/et.rs @@ -645,5 +645,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Resume", ""), ("Invalid file name", ""), ("one-way-file-transfer-tip", ""), + ("Authentication Required", ""), + ("Authenticate", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eu.rs b/src/lang/eu.rs index 9213c9689ebe..7c953ebe46e1 100644 --- a/src/lang/eu.rs +++ b/src/lang/eu.rs @@ -645,5 +645,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Resume", ""), ("Invalid file name", ""), ("one-way-file-transfer-tip", ""), + ("Authentication Required", ""), + ("Authenticate", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index 47988a95d929..a97566d99a13 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -645,5 +645,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Resume", ""), ("Invalid file name", ""), ("one-way-file-transfer-tip", ""), + ("Authentication Required", ""), + ("Authenticate", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 688bcf25f7cb..6a9ca8d8de20 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -645,5 +645,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Resume", ""), ("Invalid file name", ""), ("one-way-file-transfer-tip", ""), + ("Authentication Required", ""), + ("Authenticate", ""), ].iter().cloned().collect(); } diff --git a/src/lang/he.rs b/src/lang/he.rs index 7fc4758e1490..5a42c4257c70 100644 --- a/src/lang/he.rs +++ b/src/lang/he.rs @@ -645,5 +645,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Resume", ""), ("Invalid file name", ""), ("one-way-file-transfer-tip", ""), + ("Authentication Required", ""), + ("Authenticate", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hr.rs b/src/lang/hr.rs index 4671a0d47f97..9a2b8e3a1a02 100644 --- a/src/lang/hr.rs +++ b/src/lang/hr.rs @@ -645,5 +645,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Resume", ""), ("Invalid file name", ""), ("one-way-file-transfer-tip", ""), + ("Authentication Required", ""), + ("Authenticate", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 2ceb6bce4ce9..fb748b6b80dd 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -645,5 +645,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Resume", ""), ("Invalid file name", ""), ("one-way-file-transfer-tip", ""), + ("Authentication Required", ""), + ("Authenticate", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index 168c78117f78..9ed642459c15 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -645,5 +645,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Resume", ""), ("Invalid file name", ""), ("one-way-file-transfer-tip", ""), + ("Authentication Required", ""), + ("Authenticate", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index 08c00ee1ffbf..613dcea94c3a 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -645,5 +645,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Resume", "Riprendi"), ("Invalid file name", "Nome file non valido"), ("one-way-file-transfer-tip", ""), + ("Authentication Required", ""), + ("Authenticate", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index e58ff25f78ad..a63687c68f7b 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -645,5 +645,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Resume", "再開"), ("Invalid file name", "無効なファイル名"), ("one-way-file-transfer-tip", ""), + ("Authentication Required", ""), + ("Authenticate", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index daf85e632245..417073d05e1d 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -645,5 +645,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Resume", ""), ("Invalid file name", ""), ("one-way-file-transfer-tip", ""), + ("Authentication Required", ""), + ("Authenticate", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index 85829e84a276..a23d34448304 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -645,5 +645,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Resume", ""), ("Invalid file name", ""), ("one-way-file-transfer-tip", ""), + ("Authentication Required", ""), + ("Authenticate", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lt.rs b/src/lang/lt.rs index 7387d34e3235..b818cb7e76ea 100644 --- a/src/lang/lt.rs +++ b/src/lang/lt.rs @@ -645,5 +645,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Resume", ""), ("Invalid file name", ""), ("one-way-file-transfer-tip", ""), + ("Authentication Required", ""), + ("Authenticate", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lv.rs b/src/lang/lv.rs index 0d8b193876b1..a89590bf4f15 100644 --- a/src/lang/lv.rs +++ b/src/lang/lv.rs @@ -645,5 +645,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Resume", "Atsākt"), ("Invalid file name", "Nederīgs faila nosaukums"), ("one-way-file-transfer-tip", ""), + ("Authentication Required", ""), + ("Authenticate", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nb.rs b/src/lang/nb.rs index f3c4ed410d6a..eb2e21b683c1 100644 --- a/src/lang/nb.rs +++ b/src/lang/nb.rs @@ -645,5 +645,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Resume", ""), ("Invalid file name", ""), ("one-way-file-transfer-tip", ""), + ("Authentication Required", ""), + ("Authenticate", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nl.rs b/src/lang/nl.rs index c6d7bcbd3614..5b2eee79e92a 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -645,5 +645,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Resume", "Hervatten"), ("Invalid file name", "Ongeldige bestandsnaam"), ("one-way-file-transfer-tip", ""), + ("Authentication Required", ""), + ("Authenticate", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 59e4cbad2a11..99bf570848f0 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -645,5 +645,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Resume", "Wznów"), ("Invalid file name", "Nieprawidłowa nazwa pliku"), ("one-way-file-transfer-tip", ""), + ("Authentication Required", ""), + ("Authenticate", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index d172cb4f9115..534efe12e68b 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -645,5 +645,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Resume", ""), ("Invalid file name", ""), ("one-way-file-transfer-tip", ""), + ("Authentication Required", ""), + ("Authenticate", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index 6a5300028f4a..4396dd40de38 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -645,5 +645,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Resume", "Continuar"), ("Invalid file name", "Nome de arquivo inválido"), ("one-way-file-transfer-tip", ""), + ("Authentication Required", ""), + ("Authenticate", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ro.rs b/src/lang/ro.rs index 7b3ba2b2881b..ea1d98880147 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -645,5 +645,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Resume", ""), ("Invalid file name", ""), ("one-way-file-transfer-tip", ""), + ("Authentication Required", ""), + ("Authenticate", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 59a49959584b..f7db94bd2984 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -645,5 +645,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Resume", "Продолжить"), ("Invalid file name", "Неверное имя файла"), ("one-way-file-transfer-tip", ""), + ("Authentication Required", ""), + ("Authenticate", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 7b4678423714..e7b17c6aea80 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -645,5 +645,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Resume", "Obnoviť"), ("Invalid file name", "Nesprávny názov súboru"), ("one-way-file-transfer-tip", ""), + ("Authentication Required", ""), + ("Authenticate", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sl.rs b/src/lang/sl.rs index 637859cd4017..acbaf2ea9dba 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -645,5 +645,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Resume", ""), ("Invalid file name", ""), ("one-way-file-transfer-tip", ""), + ("Authentication Required", ""), + ("Authenticate", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sq.rs b/src/lang/sq.rs index fe9a30c0cc7e..aca5b6a7aceb 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -645,5 +645,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Resume", ""), ("Invalid file name", ""), ("one-way-file-transfer-tip", ""), + ("Authentication Required", ""), + ("Authenticate", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sr.rs b/src/lang/sr.rs index 906dc0d3f2a2..c12555c97d22 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -645,5 +645,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Resume", ""), ("Invalid file name", ""), ("one-way-file-transfer-tip", ""), + ("Authentication Required", ""), + ("Authenticate", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sv.rs b/src/lang/sv.rs index e877aca0cde2..837f702f8896 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -645,5 +645,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Resume", ""), ("Invalid file name", ""), ("one-way-file-transfer-tip", ""), + ("Authentication Required", ""), + ("Authenticate", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index 7b08bf3a3bd4..43fbbcfa5097 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -645,5 +645,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Resume", ""), ("Invalid file name", ""), ("one-way-file-transfer-tip", ""), + ("Authentication Required", ""), + ("Authenticate", ""), ].iter().cloned().collect(); } diff --git a/src/lang/th.rs b/src/lang/th.rs index 1b818f479e77..1c5b09333774 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -645,5 +645,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Resume", ""), ("Invalid file name", ""), ("one-way-file-transfer-tip", ""), + ("Authentication Required", ""), + ("Authenticate", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index 26085f539319..10fa46880f44 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -645,5 +645,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Resume", ""), ("Invalid file name", ""), ("one-way-file-transfer-tip", ""), + ("Authentication Required", ""), + ("Authenticate", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 690792dbb5f3..812d5f8616e6 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -645,5 +645,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Resume", "繼續"), ("Invalid file name", "無效文件名"), ("one-way-file-transfer-tip", ""), + ("Authentication Required", ""), + ("Authenticate", ""), ].iter().cloned().collect(); } diff --git a/src/lang/uk.rs b/src/lang/uk.rs index ccc73b7b049b..aa34887be371 100644 --- a/src/lang/uk.rs +++ b/src/lang/uk.rs @@ -645,5 +645,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Resume", ""), ("Invalid file name", ""), ("one-way-file-transfer-tip", ""), + ("Authentication Required", ""), + ("Authenticate", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index 408060a09c3c..4f7bcf7d1646 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -645,5 +645,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Resume", ""), ("Invalid file name", ""), ("one-way-file-transfer-tip", ""), + ("Authentication Required", ""), + ("Authenticate", ""), ].iter().cloned().collect(); } diff --git a/src/platform/gtk_sudo.rs b/src/platform/gtk_sudo.rs new file mode 100644 index 000000000000..a3727134e35f --- /dev/null +++ b/src/platform/gtk_sudo.rs @@ -0,0 +1,774 @@ +// https://github.com/aarnt/qt-sudo +// Sometimes reboot is needed to refresh sudoers. + +use crate::lang::translate; +use gtk::{glib, prelude::*}; +use hbb_common::{ + anyhow::{bail, Error}, + log, ResultType, +}; +use nix::{ + libc::{fcntl, kill}, + pty::{forkpty, ForkptyResult}, + sys::{ + signal::Signal, + wait::{waitpid, WaitPidFlag}, + }, + unistd::{execvp, setsid, Pid}, +}; +use std::{ + ffi::CString, + fs::File, + io::{Read, Write}, + os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd}, + sync::{ + mpsc::{channel, Receiver, Sender}, + Arc, Mutex, + }, +}; + +const EXIT_CODE: i32 = -1; + +enum Message { + PasswordPrompt(String), + Password((String, String)), + ErrorDialog(String), + Cancel, + Exit(i32), +} + +pub fn run(cmds: Vec<&str>) -> ResultType<()> { + // rustdesk service kill `rustdesk --` processes + let second_arg = std::env::args().nth(1).unwrap_or_default(); + let cmd_mode = + second_arg.starts_with("--") && second_arg != "--tray" && second_arg != "--no-server"; + let mod_arg = if cmd_mode { "cmd" } else { "gui" }; + let mut args = vec!["-gtk-sudo", mod_arg]; + args.append(&mut cmds.clone()); + let mut child = crate::run_me(args)?; + let exit_status = child.wait()?; + if exit_status.success() { + Ok(()) + } else { + bail!("child exited with status: {:?}", exit_status); + } +} + +pub fn exec() { + let mut args = vec![]; + for arg in std::env::args().skip(3) { + args.push(arg); + } + let cmd_mode = std::env::args().nth(2) == Some("cmd".to_string()); + if cmd_mode { + cmd(args); + } else { + ui(args); + } +} + +fn cmd(args: Vec) { + match unsafe { forkpty(None, None) } { + Ok(forkpty_result) => match forkpty_result { + ForkptyResult::Parent { child, master } => { + if let Err(e) = cmd_parent(child, master) { + log::error!("Parent error: {:?}", e); + kill_child(child); + std::process::exit(EXIT_CODE); + } + } + ForkptyResult::Child => { + if let Err(e) = child(None, args) { + log::error!("Child error: {:?}", e); + std::process::exit(EXIT_CODE); + } + } + }, + Err(err) => { + log::error!("forkpty error: {:?}", err); + std::process::exit(EXIT_CODE); + } + } +} + +fn ui(args: Vec) { + // https://docs.gtk.org/gtk4/ctor.Application.new.html + // https://docs.gtk.org/gio/type_func.Application.id_is_valid.html + let application = gtk::Application::new(None, Default::default()); + + let (tx_to_ui, rx_to_ui) = channel::(); + let (tx_from_ui, rx_from_ui) = channel::(); + + let rx_to_ui = Arc::new(Mutex::new(rx_to_ui)); + let tx_from_ui = Arc::new(Mutex::new(tx_from_ui)); + + let rx_to_ui_clone = rx_to_ui.clone(); + let tx_from_ui_clone = tx_from_ui.clone(); + + let username = Arc::new(Mutex::new(crate::platform::get_active_username())); + let username_clone = username.clone(); + let first_prompt = Arc::new(Mutex::new(true)); + + application.connect_activate(glib::clone!(@weak application =>move |_| { + let rx_to_ui = rx_to_ui_clone.clone(); + let tx_from_ui = tx_from_ui_clone.clone(); + let last_password = Arc::new(Mutex::new(String::new())); + let username = username_clone.clone(); + let first_prompt = first_prompt.clone(); + + glib::timeout_add_local(std::time::Duration::from_millis(50), move || { + if let Ok(msg) = rx_to_ui.lock().unwrap().try_recv() { + match msg { + Message::PasswordPrompt(err_msg) => { + let last_pwd = last_password.lock().unwrap().clone(); + let username = username.lock().unwrap().clone(); + let first = first_prompt.lock().unwrap().clone(); + *first_prompt.lock().unwrap() = false; + if let Some((username, password)) = password_prompt(&username, &last_pwd, &err_msg, first) { + *last_password.lock().unwrap() = password.clone(); + if let Err(e) = tx_from_ui + .lock() + .unwrap() + .send(Message::Password((username, password))) { + error_dialog_and_exit(&format!("Channel error: {e:?}"), EXIT_CODE); + } + } else { + if let Err(e) = tx_from_ui.lock().unwrap().send(Message::Cancel) { + error_dialog_and_exit(&format!("Channel error: {e:?}"), EXIT_CODE); + } + } + } + Message::ErrorDialog(err_msg) => { + error_dialog_and_exit(&err_msg, EXIT_CODE); + } + Message::Exit(code) => { + log::info!("Exit code: {}", code); + std::process::exit(code); + } + _ => {} + } + } + glib::ControlFlow::Continue + }); + })); + + let tx_to_ui_clone = tx_to_ui.clone(); + std::thread::spawn(move || { + let acitve_user = crate::platform::get_active_username(); + let mut initial_password = None; + if acitve_user != "root" { + if let Err(e) = tx_to_ui_clone.send(Message::PasswordPrompt("".to_string())) { + log::error!("Channel error: {e:?}"); + std::process::exit(EXIT_CODE); + } + match rx_from_ui.recv() { + Ok(Message::Password((user, password))) => { + *username.lock().unwrap() = user; + initial_password = Some(password); + } + Ok(Message::Cancel) => { + log::info!("User canceled"); + std::process::exit(EXIT_CODE); + } + _ => { + log::error!("Unexpected message"); + std::process::exit(EXIT_CODE); + } + } + } + let username = username.lock().unwrap().clone(); + let su_user = if username == acitve_user { + None + } else { + Some(username) + }; + match unsafe { forkpty(None, None) } { + Ok(forkpty_result) => match forkpty_result { + ForkptyResult::Parent { child, master } => { + if let Err(e) = ui_parent( + child, + master, + tx_to_ui_clone, + rx_from_ui, + su_user.is_some(), + initial_password, + ) { + log::error!("Parent error: {:?}", e); + kill_child(child); + std::process::exit(EXIT_CODE); + } + } + ForkptyResult::Child => { + if let Err(e) = child(su_user, args) { + log::error!("Child error: {:?}", e); + std::process::exit(EXIT_CODE); + } + } + }, + Err(err) => { + log::error!("forkpty error: {:?}", err); + if let Err(e) = + tx_to_ui.send(Message::ErrorDialog(format!("Forkpty error: {:?}", err))) + { + log::error!("Channel error: {e:?}"); + std::process::exit(EXIT_CODE); + } + } + } + }); + + let _holder = application.hold(); + let args: Vec<&str> = vec![]; + application.run_with_args(&args); + log::debug!("exit from gtk::Application::run_with_args"); + std::process::exit(EXIT_CODE); +} + +fn cmd_parent(child: Pid, master: OwnedFd) -> ResultType<()> { + let raw_fd = master.as_raw_fd(); + if unsafe { fcntl(raw_fd, nix::libc::F_SETFL, nix::libc::O_NONBLOCK) } != 0 { + let errno = std::io::Error::last_os_error(); + bail!("fcntl error: {errno:?}"); + } + let mut file = unsafe { File::from_raw_fd(raw_fd) }; + let mut stdout = std::io::stdout(); + let stdin = std::io::stdin(); + let stdin_fd = stdin.as_raw_fd(); + let old_termios = termios::Termios::from_fd(stdin_fd)?; + turn_off_echo(stdin_fd).ok(); + shutdown_hooks::add_shutdown_hook(turn_on_echo_shutdown_hook); + let (tx, rx) = channel::>(); + std::thread::spawn(move || loop { + let mut line = String::default(); + match stdin.read_line(&mut line) { + Ok(0) => { + kill_child(child); + break; + } + Ok(_) => { + if let Err(e) = tx.send(line.as_bytes().to_vec()) { + log::error!("Channel error: {e:?}"); + kill_child(child); + break; + } + } + Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {} + Err(e) => { + log::info!("Failed to read stdin: {e:?}"); + kill_child(child); + break; + } + }; + }); + loop { + let mut buf = [0; 1024]; + match file.read(&mut buf) { + Ok(0) => { + log::info!("read from child: EOF"); + break; + } + Ok(n) => { + let buf = String::from_utf8_lossy(&buf[..n]).to_string(); + print!("{}", buf); + if let Err(e) = stdout.flush() { + log::error!("flush failed: {e:?}"); + kill_child(child); + break; + } + } + Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { + std::thread::sleep(std::time::Duration::from_millis(50)); + } + Err(e) => { + // Child process is dead + log::info!("Read child error: {:?}", e); + break; + } + } + match rx.try_recv() { + Ok(v) => { + if let Err(e) = file.write_all(&v) { + log::error!("write error: {e:?}"); + kill_child(child); + break; + } + } + Err(e) => match e { + std::sync::mpsc::TryRecvError::Empty => {} + std::sync::mpsc::TryRecvError::Disconnected => { + log::error!("receive error: {e:?}"); + kill_child(child); + break; + } + }, + } + } + + // Wait for child process + let status = waitpid(child, None); + log::info!("waitpid status: {:?}", status); + let mut code = EXIT_CODE; + match status { + Ok(s) => match s { + nix::sys::wait::WaitStatus::Exited(_pid, status) => { + code = status; + } + _ => {} + }, + Err(_) => {} + } + termios::tcsetattr(stdin_fd, termios::TCSANOW, &old_termios).ok(); + std::process::exit(code); +} + +fn ui_parent( + child: Pid, + master: OwnedFd, + tx_to_ui: Sender, + rx_from_ui: Receiver, + is_su: bool, + initial_password: Option, +) -> ResultType<()> { + let mut initial_password = initial_password; + let raw_fd = master.as_raw_fd(); + if unsafe { fcntl(raw_fd, nix::libc::F_SETFL, nix::libc::O_NONBLOCK) } != 0 { + let errno = std::io::Error::last_os_error(); + tx_to_ui.send(Message::ErrorDialog(format!("fcntl error: {errno:?}")))?; + bail!("fcntl error: {errno:?}"); + } + let mut file = unsafe { File::from_raw_fd(raw_fd) }; + + let mut first = initial_password.is_none(); + let mut su_password_sent = false; + let mut saved_output = String::default(); + loop { + let mut buf = [0; 1024]; + match file.read(&mut buf) { + Ok(0) => { + log::info!("read from child: EOF"); + break; + } + Ok(n) => { + saved_output = String::default(); + let buf = String::from_utf8_lossy(&buf[..n]).trim().to_string(); + let last_line = buf.lines().last().unwrap_or(&buf).trim().to_string(); + log::info!("read from child: {}", buf); + + if last_line.starts_with("sudo:") || last_line.starts_with("su:") { + if let Err(e) = tx_to_ui.send(Message::ErrorDialog(last_line)) { + log::error!("Channel error: {e:?}"); + kill_child(child); + } + break; + } else if last_line.ends_with(":") { + match get_echo_turn_off(raw_fd) { + Ok(true) => { + log::debug!("get_echo_turn_off ok"); + if let Some(password) = initial_password.clone() { + let v = format!("{}\n", password); + if let Err(e) = file.write_all(v.as_bytes()) { + let e = format!("Failed to send password: {e:?}"); + if let Err(e) = tx_to_ui.send(Message::ErrorDialog(e)) { + log::error!("Channel error: {e:?}"); + } + kill_child(child); + break; + } + if is_su && !su_password_sent { + su_password_sent = true; + continue; + } + initial_password = None; + continue; + } + // In fact, su mode can only input password once + let err_msg = if first { "" } else { "Sorry, try again." }; + first = false; + if let Err(e) = + tx_to_ui.send(Message::PasswordPrompt(err_msg.to_string())) + { + log::error!("Channel error: {e:?}"); + kill_child(child); + break; + } + match rx_from_ui.recv() { + Ok(Message::Password((_, password))) => { + let v = format!("{}\n", password); + if let Err(e) = file.write_all(v.as_bytes()) { + let e = format!("Failed to send password: {e:?}"); + if let Err(e) = tx_to_ui.send(Message::ErrorDialog(e)) { + log::error!("Channel error: {e:?}"); + } + kill_child(child); + break; + } + } + Ok(Message::Cancel) => { + log::info!("User canceled"); + kill_child(child); + break; + } + _ => { + log::error!("Unexpected message"); + break; + } + } + } + Ok(false) => log::warn!("get_echo_turn_off timeout"), + Err(e) => log::error!("get_echo_turn_off error: {:?}", e), + } + } else { + saved_output = buf.clone(); + if !last_line.is_empty() && initial_password.is_some() { + log::error!("received not empty line: {last_line}, clear initial password"); + initial_password = None; + } + } + } + Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { + std::thread::sleep(std::time::Duration::from_millis(50)); + } + Err(e) => { + // Child process is dead + log::debug!("Read error: {:?}", e); + break; + } + } + } + + // Wait for child process + let status = waitpid(child, None); + log::info!("waitpid status: {:?}", status); + let mut code = EXIT_CODE; + match status { + Ok(s) => match s { + nix::sys::wait::WaitStatus::Exited(_pid, status) => { + code = status; + } + _ => {} + }, + Err(_) => {} + } + + if code != 0 && !saved_output.is_empty() { + if let Err(e) = tx_to_ui.send(Message::ErrorDialog(saved_output.clone())) { + log::error!("Channel error: {e:?}"); + std::process::exit(code); + } + return Ok(()); + } + if let Err(e) = tx_to_ui.send(Message::Exit(code)) { + log::error!("Channel error: {e:?}"); + std::process::exit(code); + } + Ok(()) +} + +fn child(su_user: Option, args: Vec) -> ResultType<()> { + // https://doc.rust-lang.org/std/env/consts/constant.OS.html + let os = std::env::consts::OS; + let bsd = os == "freebsd" || os == "dragonfly" || os == "netbsd" || os == "openbad"; + let mut params = vec!["sudo".to_string()]; + if su_user.is_some() { + params.push("-S".to_string()); + } + params.push("/bin/sh".to_string()); + params.push("-c".to_string()); + + let command = args + .iter() + .map(|s| { + if su_user.is_some() { + s.to_string() + } else { + quote_shell_arg(s, true) + } + }) + .collect::>() + .join(" "); + let mut command = if bsd { + let lc = match std::env::var("LC_ALL") { + Ok(lc_all) => { + if lc_all.contains('\'') { + eprintln!( + "sudo: Detected attempt to inject privileged command via LC_ALL env({lc_all}). Exiting!\n", + ); + std::process::exit(EXIT_CODE); + } + format!("LC_ALL='{lc_all}' ") + } + Err(_) => { + format!("unset LC_ALL;") + } + }; + format!("{}exec {}", lc, command) + } else { + command.to_string() + }; + if su_user.is_some() { + command = format!("'{}'", quote_shell_arg(&command, false)); + } + params.push(command); + std::env::set_var("LC_ALL", "C.UTF-8"); + + if let Some(user) = &su_user { + let su_subcommand = params + .iter() + .map(|p| p.to_string()) + .collect::>() + .join(" "); + params = vec![ + "su".to_string(), + "-".to_string(), + user.to_string(), + "-c".to_string(), + su_subcommand, + ]; + } + + // allow failure here + let _ = setsid(); + let mut cparams = vec![]; + for param in ¶ms { + cparams.push(CString::new(param.as_str())?); + } + let su_or_sudo = if su_user.is_some() { "su" } else { "sudo" }; + let res = execvp(CString::new(su_or_sudo)?.as_c_str(), &cparams); + eprintln!("sudo: execvp error: {:?}", res); + std::process::exit(EXIT_CODE); +} + +fn get_echo_turn_off(fd: RawFd) -> Result { + let tios = termios::Termios::from_fd(fd)?; + for _ in 0..10 { + if tios.c_lflag & termios::ECHO == 0 { + return Ok(true); + } + std::thread::sleep(std::time::Duration::from_millis(10)); + } + Ok(false) +} + +fn turn_off_echo(fd: RawFd) -> Result<(), Error> { + use termios::*; + let mut termios = Termios::from_fd(fd)?; + // termios.c_lflag &= !(ECHO | ECHONL | ICANON | IEXTEN); + termios.c_lflag &= !ECHO; + tcsetattr(fd, TCSANOW, &termios)?; + Ok(()) +} + +pub extern "C" fn turn_on_echo_shutdown_hook() { + let fd = std::io::stdin().as_raw_fd(); + if let Ok(mut termios) = termios::Termios::from_fd(fd) { + termios.c_lflag |= termios::ECHO; + termios::tcsetattr(fd, termios::TCSANOW, &termios).ok(); + } +} + +fn kill_child(child: Pid) { + unsafe { kill(child.as_raw(), Signal::SIGINT as _) }; + let mut res = 0; + + for _ in 0..10 { + match waitpid(child, Some(WaitPidFlag::WNOHANG)) { + Ok(_) => { + res = 1; + break; + } + Err(_) => (), + } + std::thread::sleep(std::time::Duration::from_millis(100)); + } + + if res == 0 { + log::info!("Force killing child process"); + unsafe { kill(child.as_raw(), Signal::SIGKILL as _) }; + } +} + +fn password_prompt( + username: &str, + last_password: &str, + err: &str, + show_edit: bool, +) -> Option<(String, String)> { + let dialog = gtk::Dialog::builder() + .title(crate::get_app_name()) + .modal(true) + .build(); + // https://docs.gtk.org/gtk4/method.Dialog.set_default_response.html + dialog.set_default_response(gtk::ResponseType::Ok); + let content_area = dialog.content_area(); + + let label = gtk::Label::builder() + .label(translate("Authentication Required".to_string())) + .margin_top(10) + .build(); + content_area.add(&label); + + let image = gtk::Image::from_icon_name(Some("avatar-default-symbolic"), gtk::IconSize::Dialog); + image.set_margin_top(10); + content_area.add(&image); + + let user_label = gtk::Label::new(Some(username)); + let edit_button = gtk::Button::new(); + edit_button.set_relief(gtk::ReliefStyle::None); + let edit_icon = + gtk::Image::from_icon_name(Some("document-edit-symbolic"), gtk::IconSize::Button.into()); + edit_button.set_image(Some(&edit_icon)); + edit_button.set_can_focus(false); + let user_entry = gtk::Entry::new(); + user_entry.set_alignment(0.5); + user_entry.set_width_request(100); + let user_box = gtk::Box::new(gtk::Orientation::Horizontal, 5); + user_box.add(&user_label); + user_box.add(&edit_button); + user_box.add(&user_entry); + user_box.set_halign(gtk::Align::Center); + user_box.set_valign(gtk::Align::Center); + content_area.add(&user_box); + + edit_button.connect_clicked( + glib::clone!(@weak user_label, @weak edit_button, @weak user_entry=> move |_| { + let username = user_label.text().to_string(); + user_entry.set_text(&username); + user_label.hide(); + edit_button.hide(); + user_entry.show(); + user_entry.grab_focus(); + }), + ); + + let password_input = gtk::Entry::builder() + .visibility(false) + .input_purpose(gtk::InputPurpose::Password) + .placeholder_text(translate("Password".to_string())) + .margin_top(20) + .margin_start(30) + .margin_end(30) + .activates_default(true) + .text(last_password) + .build(); + password_input.set_alignment(0.5); + // https://docs.gtk.org/gtk3/signal.Entry.activate.html + password_input.connect_activate(glib::clone!(@weak dialog => move |_| { + dialog.response(gtk::ResponseType::Ok); + })); + content_area.add(&password_input); + + user_entry.connect_focus_out_event( + glib::clone!(@weak user_label, @weak edit_button, @weak user_entry, @weak password_input => @default-return glib::Propagation::Proceed, move |_, _| { + let username = user_entry.text().to_string(); + user_label.set_text(&username); + user_entry.hide(); + user_label.show(); + edit_button.show(); + glib::Propagation::Proceed + }), + ); + user_entry.connect_activate( + glib::clone!(@weak user_label, @weak edit_button, @weak user_entry, @weak password_input => move |_| { + let username = user_entry.text().to_string(); + user_label.set_text(&username); + user_entry.hide(); + user_label.show(); + edit_button.show(); + password_input.grab_focus(); + }), + ); + + if !err.is_empty() { + let err_label = gtk::Label::new(None); + err_label.set_markup(&format!( + "{}", + err + )); + err_label.set_selectable(true); + content_area.add(&err_label); + } + + let cancel_button = gtk::Button::builder() + .label(translate("Cancel".to_string())) + .expand(true) + .build(); + cancel_button.connect_clicked(glib::clone!(@weak dialog => move |_| { + dialog.response(gtk::ResponseType::Cancel); + })); + let authenticate_button = gtk::Button::builder() + .label(translate("Authenticate".to_string())) + .expand(true) + .build(); + authenticate_button.connect_clicked(glib::clone!(@weak dialog => move |_| { + dialog.response(gtk::ResponseType::Ok); + })); + let button_box = gtk::Box::builder() + .orientation(gtk::Orientation::Horizontal) + .expand(true) + .homogeneous(true) + .spacing(10) + .margin_top(10) + .build(); + button_box.add(&cancel_button); + button_box.add(&authenticate_button); + content_area.add(&button_box); + + content_area.set_spacing(10); + content_area.set_border_width(10); + + dialog.set_width_request(400); + dialog.show_all(); + dialog.set_position(gtk::WindowPosition::Center); + dialog.set_keep_above(true); + password_input.grab_focus(); + user_entry.hide(); + if !show_edit { + edit_button.hide(); + } + dialog.check_resize(); + let response = dialog.run(); + dialog.hide(); + + if response == gtk::ResponseType::Ok { + let username = if user_entry.get_visible() { + user_entry.text().to_string() + } else { + user_label.text().to_string() + }; + Some((username, password_input.text().to_string())) + } else { + None + } +} + +fn error_dialog_and_exit(err_msg: &str, exit_code: i32) { + log::error!("Error dialog: {err_msg}, exit code: {exit_code}"); + let dialog = gtk::MessageDialog::builder() + .message_type(gtk::MessageType::Error) + .title(crate::get_app_name()) + .text("Error") + .secondary_text(err_msg) + .modal(true) + .buttons(gtk::ButtonsType::Ok) + .build(); + dialog.set_position(gtk::WindowPosition::Center); + dialog.set_keep_above(true); + dialog.run(); + dialog.close(); + std::process::exit(exit_code); +} + +fn quote_shell_arg(arg: &str, add_splash_if_match: bool) -> String { + let mut rv = arg.to_string(); + let re = hbb_common::regex::Regex::new("(\\s|[][!\"#$&'()*,;<=>?\\^`{}|~])"); + let Ok(re) = re else { + return rv; + }; + if re.is_match(arg) { + rv = rv.replace("'", "'\\''"); + if add_splash_if_match { + rv = format!("'{}'", rv); + } + } + rv +} diff --git a/src/platform/linux.rs b/src/platform/linux.rs index 4bb666fb9c2e..9c549423045d 100644 --- a/src/platform/linux.rs +++ b/src/platform/linux.rs @@ -1,4 +1,4 @@ -use super::{CursorData, ResultType}; +use super::{gtk_sudo, CursorData, ResultType}; use desktop::Desktop; use hbb_common::config::keys::OPTION_ALLOW_LINUX_HEADLESS; pub use hbb_common::platform::linux::*; @@ -15,8 +15,6 @@ use hbb_common::{ use std::{ cell::RefCell, ffi::OsStr, - fs::File, - io::{BufRead, BufReader, Write}, path::{Path, PathBuf}, process::{Child, Command}, string::String, @@ -766,30 +764,18 @@ pub fn quit_gui() { unsafe { gtk_main_quit() }; } +/* pub fn exec_privileged(args: &[&str]) -> ResultType { Ok(Command::new("pkexec").args(args).spawn()?) } +*/ pub fn check_super_user_permission() -> ResultType { - let file = format!( - "/usr/share/{}/files/polkit", - crate::get_app_name().to_lowercase() - ); - let arg; - if Path::new(&file).is_file() { - arg = file.as_str(); - } else { - arg = "echo"; - } - // https://github.com/rustdesk/rustdesk/issues/2756 - if let Ok(status) = Command::new("pkexec").arg(arg).status() { - // https://github.com/rustdesk/rustdesk/issues/5205#issuecomment-1658059657s - Ok(status.code() != Some(126) && status.code() != Some(127)) - } else { - Ok(true) - } + gtk_sudo::run(vec!["echo"])?; + Ok(true) } +/* pub fn elevate(args: Vec<&str>) -> ResultType { let cmd = std::env::current_exe()?; match cmd.to_str() { @@ -824,6 +810,7 @@ pub fn elevate(args: Vec<&str>) -> ResultType { } } } +*/ type GtkSettingsPtr = *mut c_void; type GObjectPtr = *mut c_void; @@ -1324,21 +1311,8 @@ fn has_cmd(cmd: &str) -> bool { .unwrap_or_default() } -pub fn run_cmds_pkexec(cmds: &str) -> bool { - const DONE: &str = "RUN_CMDS_PKEXEC_DONE"; - if let Ok(output) = std::process::Command::new("pkexec") - .arg("sh") - .arg("-c") - .arg(&format!("{cmds} echo {DONE}")) - .output() - { - let out = String::from_utf8_lossy(&output.stdout); - log::debug!("cmds: {cmds}"); - log::debug!("output: {out}"); - out.contains(DONE) - } else { - false - } +pub fn run_cmds_privileged(cmds: &str) -> bool { + crate::platform::gtk_sudo::run(vec![cmds]).is_ok() } pub fn run_me_with(secs: u32) { @@ -1367,13 +1341,15 @@ fn switch_service(stop: bool) -> String { pub fn uninstall_service(show_new_window: bool, _: bool) -> bool { if !has_cmd("systemctl") { + // Failed when installed + flutter run + started by `show_new_window`. return false; } log::info!("Uninstalling service..."); let cp = switch_service(true); let app_name = crate::get_app_name().to_lowercase(); - if !run_cmds_pkexec(&format!( - "systemctl disable {app_name}; systemctl stop {app_name}; {cp}" + // systemctl kill rustdesk --tray, execute cp first + if !run_cmds_privileged(&format!( + "{cp} systemctl disable {app_name}; systemctl stop {app_name};" )) { Config::set_option("stop-service".into(), "".into()); return true; @@ -1393,8 +1369,8 @@ pub fn install_service() -> bool { log::info!("Installing service..."); let cp = switch_service(false); let app_name = crate::get_app_name().to_lowercase(); - if !run_cmds_pkexec(&format!( - "{cp} systemctl enable {app_name}; systemctl stop {app_name}; systemctl start {app_name};" + if !run_cmds_privileged(&format!( + "{cp} systemctl enable {app_name}; systemctl start {app_name};" )) { Config::set_option("stop-service".into(), "Y".into()); } @@ -1404,9 +1380,9 @@ pub fn install_service() -> bool { fn check_if_stop_service() { if Config::get_option("stop-service".into()) == "Y" { let app_name = crate::get_app_name().to_lowercase(); - allow_err!(run_cmds( + allow_err!(run_cmds(&format!( "systemctl disable {app_name}; systemctl stop {app_name}" - )); + ))); } } diff --git a/src/platform/mod.rs b/src/platform/mod.rs index 169bdb199f45..d0ddd09bf700 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -23,6 +23,9 @@ pub mod linux; #[cfg(target_os = "linux")] pub mod linux_desktop_manager; +#[cfg(target_os = "linux")] +pub mod gtk_sudo; + #[cfg(not(any(target_os = "android", target_os = "ios")))] use hbb_common::{message_proto::CursorData, ResultType}; use std::sync::{Arc, Mutex}; diff --git a/src/tray.rs b/src/tray.rs index 74c18bf7bd0b..3a3ae92f37f1 100644 --- a/src/tray.rs +++ b/src/tray.rs @@ -98,12 +98,11 @@ fn make_tray() -> hbb_common::ResultType<()> { crate::run_me::<&str>(vec![]).ok(); } #[cfg(target_os = "linux")] - if !std::process::Command::new("xdg-open") - .arg(&crate::get_uri_prefix()) - .spawn() - .is_ok() { - crate::run_me::<&str>(vec![]).ok(); + // Do not use "xdg-open", it won't read config + if crate::dbus::invoke_new_connection(crate::get_uri_prefix()).is_err() { + crate::run_me::<&str>(vec![]).ok(); + } } }; From c6e3f60a6b45160b5e8853a3bb9dde2e4cc523e7 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Thu, 19 Sep 2024 18:48:01 +0800 Subject: [PATCH 206/541] refact: flutter, ChangeNotifier, reduce rebuild (#9392) Signed-off-by: fufesou --- flutter/lib/common/widgets/peer_card.dart | 3 +++ flutter/lib/common/widgets/peer_tab_page.dart | 10 ++++------ flutter/lib/common/widgets/peers_view.dart | 6 ++++++ flutter/lib/models/peer_tab_model.dart | 9 ++++++++- 4 files changed, 21 insertions(+), 7 deletions(-) diff --git a/flutter/lib/common/widgets/peer_card.dart b/flutter/lib/common/widgets/peer_card.dart index 15ca8932d1f3..7827298b6d07 100644 --- a/flutter/lib/common/widgets/peer_card.dart +++ b/flutter/lib/common/widgets/peer_card.dart @@ -250,6 +250,9 @@ class _PeerCardState extends State<_PeerCard> color: Colors.transparent, elevation: 0, 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( () => Container( foregroundDecoration: deco.value, diff --git a/flutter/lib/common/widgets/peer_tab_page.dart b/flutter/lib/common/widgets/peer_tab_page.dart index c941a1b93d49..359750788058 100644 --- a/flutter/lib/common/widgets/peer_tab_page.dart +++ b/flutter/lib/common/widgets/peer_tab_page.dart @@ -108,7 +108,7 @@ class _PeerTabPageState extends State Widget build(BuildContext context) { final model = Provider.of(context); Widget selectionWrap(Widget widget) { - return model.multiSelectionMode ? createMultiSelectionBar() : widget; + return model.multiSelectionMode ? createMultiSelectionBar(model) : widget; } return Column( @@ -362,8 +362,7 @@ class _PeerTabPageState extends State .toList()); } - Widget createMultiSelectionBar() { - final model = Provider.of(context); + Widget createMultiSelectionBar(PeerTabModel model) { return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -381,7 +380,7 @@ class _PeerTabPageState extends State Row( children: [ selectionCount(model.selectedPeers.length), - selectAll(), + selectAll(model), closeSelection(), ], ) @@ -512,8 +511,7 @@ class _PeerTabPageState extends State ); } - Widget selectAll() { - final model = Provider.of(context); + Widget selectAll(PeerTabModel model) { return Offstage( offstage: model.selectedPeers.length >= model.currentTabCachedPeers.length, diff --git a/flutter/lib/common/widgets/peers_view.dart b/flutter/lib/common/widgets/peers_view.dart index a73ef0f0bd26..b18de82d995c 100644 --- a/flutter/lib/common/widgets/peers_view.dart +++ b/flutter/lib/common/widgets/peers_view.dart @@ -167,6 +167,9 @@ class _PeersViewState extends State<_PeersView> @override Widget build(BuildContext context) { + // We should avoid too many rebuilds. MacOS(m1, 14.6.1) on Flutter 3.19.6. + // Continious rebuilds of `ChangeNotifierProvider` will cause memory leak. + // Simple demo can reproduce this issue. return ChangeNotifierProvider( create: (context) => widget.peers, child: Consumer(builder: (context, peers, child) { @@ -245,6 +248,9 @@ class _PeersViewState extends State<_PeersView> : Container(child: visibilityChild); } + // We should avoid too many rebuilds. Win10(Some machines) on Flutter 3.19.6. + // Continious rebuilds of `ListView.builder` will cause memory leak. + // Simple demo can reproduce this issue. final Widget child = Obx(() => stateGlobal.isPortrait.isTrue ? ListView.builder( itemCount: peers.length, diff --git a/flutter/lib/models/peer_tab_model.dart b/flutter/lib/models/peer_tab_model.dart index 3c0fe636d685..7dab2574dcf5 100644 --- a/flutter/lib/models/peer_tab_model.dart +++ b/flutter/lib/models/peer_tab_model.dart @@ -184,10 +184,17 @@ class PeerTabModel with ChangeNotifier { notifyListeners(); } + // `notifyListeners()` will cause many rebuilds. + // So, we need to reduce the calls to "notifyListeners()" only when necessary. + // A better way is to use a new model. setCurrentTabCachedPeers(List peers) { Future.delayed(Duration.zero, () { + final isPreEmpty = _currentTabCachedPeers.isEmpty; _currentTabCachedPeers = peers; - notifyListeners(); + final isNowEmpty = _currentTabCachedPeers.isEmpty; + if (isPreEmpty != isNowEmpty) { + notifyListeners(); + } }); } From 47139edd81a4add7be363d234b69b27b5177d491 Mon Sep 17 00:00:00 2001 From: solokot Date: Thu, 19 Sep 2024 13:48:54 +0300 Subject: [PATCH 207/541] Update Russian translation (#9391) Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com> --- src/lang/ru.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/lang/ru.rs b/src/lang/ru.rs index f7db94bd2984..9235384f43fc 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -554,7 +554,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Open in new window", "Открыть в новом окне"), ("Show displays as individual windows", "Показывать дисплеи в отдельных окнах"), ("Use all my displays for the remote session", "Использовать все мои дисплеи для удалённого сеанса"), - ("selinux_tip", "На вашем устройстве включён SELinux, что может помешать правильной работе RustDesk на контролируемой стороне."), + ("selinux_tip", "На вашем устройстве включён SELinux, что может помешать правильной работе RustDesk на управляемой стороне."), ("Change view", "Вид"), ("Big tiles", "Большие значки"), ("Small tiles", "Маленькие значки"), @@ -641,10 +641,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Platform", "Платформа"), ("Days remaining", "Дней осталось"), ("enable-trusted-devices-tip", "Разрешить доверенным устройствам пропускать проверку подлинности 2FA"), - ("Parent directory", "Родительская директория"), + ("Parent directory", "Родительская папка"), ("Resume", "Продолжить"), - ("Invalid file name", "Неверное имя файла"), - ("one-way-file-transfer-tip", ""), + ("Invalid file name", "Неправильное имя файла"), + ("one-way-file-transfer-tip", "На управляемой стороне включена односторонняя передача файлов."), ("Authentication Required", ""), ("Authenticate", ""), ].iter().cloned().collect(); From ddd3401bd79248bc9bb4d777bdf93ee2477b53a6 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Fri, 20 Sep 2024 10:52:19 +0800 Subject: [PATCH 208/541] fix: keyboard, translate mode (#9406) hotkey, linux -> win Signed-off-by: fufesou --- src/server/input_service.rs | 33 +++++++++++++++++++++++++++------ 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/src/server/input_service.rs b/src/server/input_service.rs index 06ab61a25c0f..1527009cad24 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -1448,17 +1448,27 @@ fn translate_keyboard_mode(evt: &KeyEvent) { en.key_sequence(seq); #[cfg(any(target_os = "linux", target_os = "windows"))] { - if get_modifier_state(Key::Shift, &mut en) { - simulate_(&EventType::KeyRelease(RdevKey::ShiftLeft)); - } - if get_modifier_state(Key::RightShift, &mut en) { - simulate_(&EventType::KeyRelease(RdevKey::ShiftRight)); + #[cfg(target_os = "windows")] + let simulate_win_hot_key = is_hot_key_modifiers_down(&mut en); + #[cfg(target_os = "linux")] + let simulate_win_hot_key = false; + if !simulate_win_hot_key { + if get_modifier_state(Key::Shift, &mut en) { + simulate_(&EventType::KeyRelease(RdevKey::ShiftLeft)); + } + if get_modifier_state(Key::RightShift, &mut en) { + simulate_(&EventType::KeyRelease(RdevKey::ShiftRight)); + } } for chr in seq.chars() { // char in rust is 4 bytes. // But for this case, char comes from keyboard. We only need 2 bytes. #[cfg(target_os = "windows")] - rdev::simulate_unicode(chr as _).ok(); + if simulate_win_hot_key { + rdev::simulate_char(chr, true).ok(); + } else { + rdev::simulate_unicode(chr as _).ok(); + } #[cfg(target_os = "linux")] en.key_click(Key::Layout(chr)); } @@ -1483,6 +1493,17 @@ fn translate_keyboard_mode(evt: &KeyEvent) { } } +#[inline] +#[cfg(target_os = "windows")] +fn is_hot_key_modifiers_down(en: &mut Enigo) -> bool { + en.get_key_state(Key::Control) + || en.get_key_state(Key::RightControl) + || en.get_key_state(Key::Alt) + || en.get_key_state(Key::RightAlt) + || en.get_key_state(Key::Meta) + || en.get_key_state(Key::RWin) +} + #[cfg(target_os = "windows")] fn simulate_win2win_hotkey(code: u32, down: bool) { let unicode: u16 = (code & 0x0000FFFF) as u16; From 216a72592dcba9a14a329acf605f91caf34646ec Mon Sep 17 00:00:00 2001 From: bovirus <1262554+bovirus@users.noreply.github.com> Date: Fri, 20 Sep 2024 04:54:37 +0200 Subject: [PATCH 209/541] Update Italian language (#9403) --- src/lang/it.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/lang/it.rs b/src/lang/it.rs index 613dcea94c3a..008fb3b3518f 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -73,7 +73,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Remember password", "Ricorda password"), ("Wrong Password", "Password errata"), ("Do you want to enter again?", "Vuoi riprovare?"), - ("Connection Error", "Errore di connessione"), + ("Connection Error", "Errore connessione"), ("Error", "Errore"), ("Reset by the peer", "Reimpostata dal dispositivo remoto"), ("Connecting...", "Connessione..."), @@ -134,14 +134,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Insert Lock", "Blocco inserimento"), ("Refresh", "Aggiorna"), ("ID does not exist", "L'ID non esiste"), - ("Failed to connect to rendezvous server", "Errore di connessione al server rendezvous"), + ("Failed to connect to rendezvous server", "Errore connessione al server rendezvous"), ("Please try later", "Riprova più tardi"), ("Remote desktop is offline", "Il desktop remoto è offline"), ("Key mismatch", "La chiave non corrisponde"), ("Timeout", "Timeout"), - ("Failed to connect to relay server", "Errore di connessione al server relay"), - ("Failed to connect via rendezvous server", "Errore di connessione tramite il server rendezvous"), - ("Failed to connect via relay server", "Errore di connessione tramite il server relay"), + ("Failed to connect to relay server", "Errore connessione al server relay"), + ("Failed to connect via rendezvous server", "Errore connessione tramite il server rendezvous"), + ("Failed to connect via relay server", "Errore connessione tramite il server relay"), ("Failed to make direct connection to remote desktop", "Impossibile connettersi direttamente al desktop remoto"), ("Set Password", "Imposta password"), ("OS Password", "Password sistema operativo"), @@ -226,7 +226,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add ID", "Aggiungi ID"), ("Add Tag", "Aggiungi etichetta"), ("Unselect all tags", "Deseleziona tutte le etichette"), - ("Network error", "Errore di rete"), + ("Network error", "Errore rete"), ("Username missed", "Nome utente mancante"), ("Password missed", "Password mancante"), ("Wrong credentials", "Credenziali errate"), @@ -644,8 +644,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Parent directory", "Cartella principale"), ("Resume", "Riprendi"), ("Invalid file name", "Nome file non valido"), - ("one-way-file-transfer-tip", ""), - ("Authentication Required", ""), - ("Authenticate", ""), + ("one-way-file-transfer-tip", "Il trasferimento file unidirezionale è abilitato sul lato controllato."), + ("Authentication Required", "Richiesta autenticazione"), + ("Authenticate", "Autentica"), ].iter().cloned().collect(); } From cfd801c5d66dafdbd3329aa96cd89cee748180c1 Mon Sep 17 00:00:00 2001 From: Mr-Update <37781396+Mr-Update@users.noreply.github.com> Date: Fri, 20 Sep 2024 04:54:50 +0200 Subject: [PATCH 210/541] Update de.rs (#9401) --- src/lang/de.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/lang/de.rs b/src/lang/de.rs index 38720f53797c..d4f72c94b016 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -548,7 +548,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("pull_group_failed_tip", "Aktualisierung der Gruppe fehlgeschlagen"), ("Filter by intersection", "Nach Schnittmenge filtern"), ("Remove wallpaper during incoming sessions", "Hintergrundbild bei eingehenden Sitzungen entfernen"), - ("Test", "Test"), + ("Test", "Testen"), ("display_is_plugged_out_msg", "Der Bildschirm ist nicht angeschlossen, schalten Sie auf den ersten Bildschirm um."), ("No displays", "Keine Bildschirme"), ("Open in new window", "In einem neuen Fenster öffnen"), @@ -644,8 +644,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Parent directory", "Übergeordnetes Verzeichnis"), ("Resume", "Fortsetzen"), ("Invalid file name", "Ungültiger Dateiname"), - ("one-way-file-transfer-tip", ""), - ("Authentication Required", ""), - ("Authenticate", ""), + ("one-way-file-transfer-tip", "Die einseitige Dateiübertragung ist auf der kontrollierten Seite aktiviert."), + ("Authentication Required", "Authentifizierung erforderlich"), + ("Authenticate", "Authentifizieren"), ].iter().cloned().collect(); } From 3d5262c36f54a8648b4ae6d78293f44cd069968f Mon Sep 17 00:00:00 2001 From: 21pages Date: Fri, 20 Sep 2024 11:06:56 +0800 Subject: [PATCH 211/541] opt gtk sudo ui, fix edit button show (#9399) Signed-off-by: 21pages --- src/platform/gtk_sudo.rs | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/src/platform/gtk_sudo.rs b/src/platform/gtk_sudo.rs index a3727134e35f..0105b335c311 100644 --- a/src/platform/gtk_sudo.rs +++ b/src/platform/gtk_sudo.rs @@ -30,7 +30,7 @@ use std::{ const EXIT_CODE: i32 = -1; enum Message { - PasswordPrompt(String), + PasswordPrompt((String, bool)), Password((String, String)), ErrorDialog(String), Cancel, @@ -107,24 +107,20 @@ fn ui(args: Vec) { let username = Arc::new(Mutex::new(crate::platform::get_active_username())); let username_clone = username.clone(); - let first_prompt = Arc::new(Mutex::new(true)); application.connect_activate(glib::clone!(@weak application =>move |_| { let rx_to_ui = rx_to_ui_clone.clone(); let tx_from_ui = tx_from_ui_clone.clone(); let last_password = Arc::new(Mutex::new(String::new())); let username = username_clone.clone(); - let first_prompt = first_prompt.clone(); glib::timeout_add_local(std::time::Duration::from_millis(50), move || { if let Ok(msg) = rx_to_ui.lock().unwrap().try_recv() { match msg { - Message::PasswordPrompt(err_msg) => { + Message::PasswordPrompt((err_msg, show_edit)) => { let last_pwd = last_password.lock().unwrap().clone(); let username = username.lock().unwrap().clone(); - let first = first_prompt.lock().unwrap().clone(); - *first_prompt.lock().unwrap() = false; - if let Some((username, password)) = password_prompt(&username, &last_pwd, &err_msg, first) { + if let Some((username, password)) = password_prompt(&username, &last_pwd, &err_msg, show_edit) { *last_password.lock().unwrap() = password.clone(); if let Err(e) = tx_from_ui .lock() @@ -157,7 +153,7 @@ fn ui(args: Vec) { let acitve_user = crate::platform::get_active_username(); let mut initial_password = None; if acitve_user != "root" { - if let Err(e) = tx_to_ui_clone.send(Message::PasswordPrompt("".to_string())) { + if let Err(e) = tx_to_ui_clone.send(Message::PasswordPrompt(("".to_string(), true))) { log::error!("Channel error: {e:?}"); std::process::exit(EXIT_CODE); } @@ -385,7 +381,7 @@ fn ui_parent( let err_msg = if first { "" } else { "Sorry, try again." }; first = false; if let Err(e) = - tx_to_ui.send(Message::PasswordPrompt(err_msg.to_string())) + tx_to_ui.send(Message::PasswordPrompt((err_msg.to_string(), false))) { log::error!("Channel error: {e:?}"); kill_child(child); @@ -627,6 +623,7 @@ fn password_prompt( user_box.add(&user_entry); user_box.set_halign(gtk::Align::Center); user_box.set_valign(gtk::Align::Center); + user_box.set_vexpand(true); content_area.add(&user_box); edit_button.connect_clicked( @@ -690,21 +687,21 @@ fn password_prompt( let cancel_button = gtk::Button::builder() .label(translate("Cancel".to_string())) - .expand(true) + .hexpand(true) .build(); cancel_button.connect_clicked(glib::clone!(@weak dialog => move |_| { dialog.response(gtk::ResponseType::Cancel); })); let authenticate_button = gtk::Button::builder() .label(translate("Authenticate".to_string())) - .expand(true) + .hexpand(true) .build(); authenticate_button.connect_clicked(glib::clone!(@weak dialog => move |_| { dialog.response(gtk::ResponseType::Ok); })); let button_box = gtk::Box::builder() .orientation(gtk::Orientation::Horizontal) - .expand(true) + .hexpand(true) .homogeneous(true) .spacing(10) .margin_top(10) From 21bcfd173d299d38fcf0e956aa0dc40afaee1931 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Fri, 20 Sep 2024 11:15:19 +0800 Subject: [PATCH 212/541] fix: wayland, rdp input, mouse, scale (#9402) * fix: wayland, rdp input, mouse, scale Signed-off-by: fufesou * fix: rdp input, mouse, scale, check 0 Signed-off-by: fufesou --------- Signed-off-by: fufesou --- libs/hbb_common/src/platform/linux.rs | 12 ++++++ libs/scrap/src/wayland/pipewire.rs | 58 ++++++++++++++++++--------- src/server/input_service.rs | 22 +++++++--- src/server/rdp_input.rs | 53 ++++++++++++++++++++++-- 4 files changed, 117 insertions(+), 28 deletions(-) diff --git a/libs/hbb_common/src/platform/linux.rs b/libs/hbb_common/src/platform/linux.rs index 5e03b6816e4c..60c8714d8212 100644 --- a/libs/hbb_common/src/platform/linux.rs +++ b/libs/hbb_common/src/platform/linux.rs @@ -7,6 +7,9 @@ lazy_static::lazy_static! { pub const DISPLAY_SERVER_WAYLAND: &str = "wayland"; pub const DISPLAY_SERVER_X11: &str = "x11"; +pub const DISPLAY_DESKTOP_KDE: &str = "KDE"; + +pub const XDG_CURRENT_DESKTOP: &str = "XDG_CURRENT_DESKTOP"; pub struct Distro { pub name: String, @@ -29,6 +32,15 @@ impl Distro { } } +#[inline] +pub fn is_kde() -> bool { + if let Ok(env) = std::env::var(XDG_CURRENT_DESKTOP) { + env == DISPLAY_DESKTOP_KDE + } else { + false + } +} + #[inline] pub fn is_gdm_user(username: &str) -> bool { username == "gdm" diff --git a/libs/scrap/src/wayland/pipewire.rs b/libs/scrap/src/wayland/pipewire.rs index 640f37d0b834..2f1e2a852670 100644 --- a/libs/scrap/src/wayland/pipewire.rs +++ b/libs/scrap/src/wayland/pipewire.rs @@ -27,39 +27,40 @@ use super::screencast_portal::OrgFreedesktopPortalScreenCast as screencast_porta use lazy_static::lazy_static; lazy_static! { - pub static ref RDP_RESPONSE: Mutex> = Mutex::new(None); + pub static ref RDP_SESSION_INFO: Mutex> = Mutex::new(None); } #[inline] pub fn close_session() { - let _ = RDP_RESPONSE.lock().unwrap().take(); + let _ = RDP_SESSION_INFO.lock().unwrap().take(); } #[inline] pub fn is_rdp_session_hold() -> bool { - RDP_RESPONSE.lock().unwrap().is_some() + RDP_SESSION_INFO.lock().unwrap().is_some() } pub fn try_close_session() { - let mut rdp_res = RDP_RESPONSE.lock().unwrap(); + let mut rdp_info = RDP_SESSION_INFO.lock().unwrap(); let mut close = false; - if let Some(rdp_res) = &*rdp_res { + if let Some(rdp_info) = &*rdp_info { // If is server running and restore token is supported, there's no need to keep the session. - if is_server_running() && rdp_res.is_support_restore_token { + if is_server_running() && rdp_info.is_support_restore_token { close = true; } } if close { - *rdp_res = None; + *rdp_info = None; } } -pub struct RdpResponse { +pub struct RdpSessionInfo { pub conn: Arc, pub streams: Vec, pub fd: OwnedFd, pub session: dbus::Path<'static>, pub is_support_restore_token: bool, + pub resolution: Arc>>, } #[derive(Debug, Clone, Copy)] pub struct PwStreamInfo { @@ -69,6 +70,12 @@ pub struct PwStreamInfo { size: (usize, usize), } +impl PwStreamInfo { + pub fn get_size(&self) -> (usize, usize) { + self.size + } +} + #[derive(Debug)] pub struct DBusError(String); @@ -105,24 +112,31 @@ pub struct PipeWireCapturable { } impl PipeWireCapturable { - fn new(conn: Arc, fd: OwnedFd, stream: PwStreamInfo) -> Self { + fn new( + conn: Arc, + fd: OwnedFd, + resolution: Arc>>, + stream: PwStreamInfo, + ) -> Self { // alternative to get screen resolution as stream.size is not always correct ex: on fractional scaling // https://github.com/rustdesk/rustdesk/issues/6116#issuecomment-1817724244 - let res = get_res(Self { + let size = get_res(Self { dbus_conn: conn.clone(), fd: fd.clone(), path: stream.path, source_type: stream.source_type, position: stream.position, size: stream.size, - }); + }) + .unwrap_or(stream.size); + *resolution.lock().unwrap() = Some(size); Self { dbus_conn: conn, fd, path: stream.path, source_type: stream.source_type, position: stream.position, - size: res.unwrap_or(stream.size), + size, } } } @@ -813,7 +827,7 @@ fn on_start_response( } pub fn get_capturables() -> Result, Box> { - let mut rdp_connection = match RDP_RESPONSE.lock() { + let mut rdp_connection = match RDP_SESSION_INFO.lock() { Ok(conn) => conn, Err(err) => return Err(Box::new(err)), }; @@ -822,28 +836,36 @@ pub fn get_capturables() -> Result, Box> { let (conn, fd, streams, session, is_support_restore_token) = request_remote_desktop()?; let conn = Arc::new(conn); - let rdp_res = RdpResponse { + let rdp_info = RdpSessionInfo { conn, streams, fd, session, is_support_restore_token, + resolution: Arc::new(Mutex::new(None)), }; - *rdp_connection = Some(rdp_res); + *rdp_connection = Some(rdp_info); } - let rdp_res = match rdp_connection.as_ref() { + let rdp_info = match rdp_connection.as_ref() { Some(res) => res, None => { return Err(Box::new(DBusError("RDP response is None.".into()))); } }; - Ok(rdp_res + Ok(rdp_info .streams .clone() .into_iter() - .map(|s| PipeWireCapturable::new(rdp_res.conn.clone(), rdp_res.fd.clone(), s)) + .map(|s| { + PipeWireCapturable::new( + rdp_info.conn.clone(), + rdp_info.fd.clone(), + rdp_info.resolution.clone(), + s, + ) + }) .collect()) } diff --git a/src/server/input_service.rs b/src/server/input_service.rs index 1527009cad24..fd4291cc8f61 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -16,7 +16,7 @@ use rdev::{self, EventType, Key as RdevKey, KeyCode, RawKey}; #[cfg(target_os = "macos")] use rdev::{CGEventSourceStateID, CGEventTapLocation, VirtualInput}; #[cfg(target_os = "linux")] -use scrap::wayland::pipewire::RDP_RESPONSE; +use scrap::wayland::pipewire::RDP_SESSION_INFO; use std::{ convert::TryFrom, ops::{Deref, DerefMut, Sub}, @@ -521,15 +521,25 @@ pub async fn setup_uinput(minx: i32, maxx: i32, miny: i32, maxy: i32) -> ResultT #[cfg(target_os = "linux")] pub async fn setup_rdp_input() -> ResultType<(), Box> { let mut en = ENIGO.lock()?; - let rdp_res_lock = RDP_RESPONSE.lock()?; - let rdp_res = rdp_res_lock.as_ref().ok_or("RDP response is None")?; + let rdp_info_lock = RDP_SESSION_INFO.lock()?; + let rdp_info = rdp_info_lock.as_ref().ok_or("RDP session is None")?; - let keyboard = RdpInputKeyboard::new(rdp_res.conn.clone(), rdp_res.session.clone())?; + let keyboard = RdpInputKeyboard::new(rdp_info.conn.clone(), rdp_info.session.clone())?; en.set_custom_keyboard(Box::new(keyboard)); log::info!("RdpInput keyboard created"); - if let Some(stream) = rdp_res.streams.clone().into_iter().next() { - let mouse = RdpInputMouse::new(rdp_res.conn.clone(), rdp_res.session.clone(), stream)?; + if let Some(stream) = rdp_info.streams.clone().into_iter().next() { + let resolution = rdp_info + .resolution + .lock() + .unwrap() + .unwrap_or(stream.get_size()); + let mouse = RdpInputMouse::new( + rdp_info.conn.clone(), + rdp_info.session.clone(), + stream, + resolution, + )?; en.set_custom_mouse(Box::new(mouse)); log::info!("RdpInput mouse created"); } diff --git a/src/server/rdp_input.rs b/src/server/rdp_input.rs index 1a0a64054b98..910a192761df 100644 --- a/src/server/rdp_input.rs +++ b/src/server/rdp_input.rs @@ -8,6 +8,8 @@ use std::collections::HashMap; use std::sync::Arc; pub mod client { + use hbb_common::platform::linux::is_kde; + use super::*; const EVDEV_MOUSE_LEFT: i32 = 272; @@ -67,6 +69,8 @@ pub mod client { conn: Arc, session: Path<'static>, stream: PwStreamInfo, + resolution: (usize, usize), + scale: Option, } impl RdpInputMouse { @@ -74,11 +78,32 @@ pub mod client { conn: Arc, session: Path<'static>, stream: PwStreamInfo, + resolution: (usize, usize), ) -> ResultType { + // https://github.com/rustdesk/rustdesk/pull/9019#issuecomment-2295252388 + // There may be a bug in Rdp input on Gnome util Ubuntu 24.04 (Gnome 46) + // + // eg. Resultion 800x600, Fractional scale: 200% (logic size: 400x300) + // https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.impl.portal.RemoteDesktop.html#:~:text=new%20pointer%20position-,in%20the%20streams%20logical%20coordinate%20space,-. + // Then (x,y) in `mouse_move_to()` and `mouse_move_relative()` should be scaled to the logic size(stream.get_size()), which is from (0,0) to (400,300). + // For Ubuntu 24.04(Gnome 46), (x,y) is restricted from (0,0) to (400,300), but the actual range in screen is: + // Logic coordinate from (0,0) to (200x150). + // Or physical coordinate from (0,0) to (400,300). + let scale = if is_kde() { + if resolution.0 == 0 || stream.get_size().0 == 0 { + Some(1.0f64) + } else { + Some(resolution.0 as f64 / stream.get_size().0 as f64) + } + } else { + None + }; Ok(Self { conn, session, stream, + resolution, + scale, }) } } @@ -93,24 +118,44 @@ pub mod client { } fn mouse_move_to(&mut self, x: i32, y: i32) { + let x = if let Some(s) = self.scale { + x as f64 / s + } else { + x as f64 + }; + let y = if let Some(s) = self.scale { + y as f64 / s + } else { + y as f64 + }; let portal = get_portal(&self.conn); let _ = remote_desktop_portal::notify_pointer_motion_absolute( &portal, &self.session, HashMap::new(), self.stream.path as u32, - x as f64, - y as f64, + x, + y, ); } fn mouse_move_relative(&mut self, x: i32, y: i32) { + let x = if let Some(s) = self.scale { + x as f64 / s + } else { + x as f64 + }; + let y = if let Some(s) = self.scale { + y as f64 / s + } else { + y as f64 + }; let portal = get_portal(&self.conn); let _ = remote_desktop_portal::notify_pointer_motion( &portal, &self.session, HashMap::new(), - x as f64, - y as f64, + x, + y, ); } fn mouse_down(&mut self, button: MouseButton) -> enigo::ResultType { From b93d4ce3fc2633351b7829db1b396d65fcf296a5 Mon Sep 17 00:00:00 2001 From: jkh0kr Date: Fri, 20 Sep 2024 16:16:20 +0900 Subject: [PATCH 213/541] Update ko.rs (#9411) Add Korean language --- src/lang/ko.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 417073d05e1d..9d9c29f9af2a 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -641,11 +641,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Platform", "플랫폼"), ("Days remaining", "일 남음"), ("enable-trusted-devices-tip", "신뢰할 수 있는 기기에서 2FA 검증 건너뛰기"), - ("Parent directory", ""), - ("Resume", ""), - ("Invalid file name", ""), - ("one-way-file-transfer-tip", ""), - ("Authentication Required", ""), - ("Authenticate", ""), + ("Parent directory", "상위 디렉토리"), + ("Resume", "재개"), + ("Invalid file name", "잘못된 파일 이름"), + ("one-way-file-transfer-tip", "단방향 파일 전송은 제어되는 쪽에서 활성화됩니다."), + ("Authentication Required", "인증 필요함"), + ("Authenticate", "인증"), ].iter().cloned().collect(); } From 2e314bf032938fb57a82a8bed3285b490da5b9c9 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Fri, 20 Sep 2024 17:38:29 +0800 Subject: [PATCH 214/541] disable init clipboard sync by default --- libs/hbb_common/src/config.rs | 14 ++++++++++++-- src/client/io_loop.rs | 15 ++++++--------- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/libs/hbb_common/src/config.rs b/libs/hbb_common/src/config.rs index 0e91ecf42c3f..f0f7ec7317ec 100644 --- a/libs/hbb_common/src/config.rs +++ b/libs/hbb_common/src/config.rs @@ -296,6 +296,8 @@ pub struct PeerConfig { pub keyboard_mode: String, #[serde(flatten)] pub view_only: ViewOnly, + #[serde(flatten)] + pub sync_init_clipboard: SyncInitClipboard, // Mouse wheel or touchpad scroll mode #[serde( default = "PeerConfig::default_reverse_mouse_wheel", @@ -373,6 +375,7 @@ impl Default for PeerConfig { ui_flutter: Default::default(), info: Default::default(), transfer: Default::default(), + sync_init_clipboard: Default::default(), } } } @@ -1462,6 +1465,13 @@ serde_field_bool!( "ViewOnly::default_view_only" ); +serde_field_bool!( + SyncInitClipboard, + "sync-init-clipboard", + default_sync_init_clipboard, + "SyncInitClipboard::default_sync_init_clipboard" +); + #[derive(Debug, Default, Serialize, Deserialize, Clone)] pub struct LocalConfig { #[serde(default, deserialize_with = "deserialize_string")] @@ -2156,6 +2166,7 @@ pub mod keys { pub const OPTION_CUSTOM_IMAGE_QUALITY: &str = "custom_image_quality"; pub const OPTION_CUSTOM_FPS: &str = "custom-fps"; pub const OPTION_CODEC_PREFERENCE: &str = "codec-preference"; + pub const OPTION_SYNC_INIT_CLIPBOARD: &str = "sync-init-clipboard"; pub const OPTION_THEME: &str = "theme"; pub const OPTION_LANGUAGE: &str = "lang"; pub const OPTION_REMOTE_MENUBAR_DRAG_LEFT: &str = "remote-menubar-drag-left"; @@ -2219,7 +2230,6 @@ pub mod keys { pub const OPTION_DEFAULT_CONNECT_PASSWORD: &str = "default-connect-password"; pub const OPTION_HIDE_TRAY: &str = "hide-tray"; pub const OPTION_ONE_WAY_CLIPBOARD_REDIRECTION: &str = "one-way-clipboard-redirection"; - pub const OPTION_ENABLE_CLIPBOARD_INIT_SYNC: &str = "enable-clipboard-init-sync"; pub const OPTION_ALLOW_LOGON_SCREEN_PASSWORD: &str = "allow-logon-screen-password"; pub const OPTION_ONE_WAY_FILE_TRANSFER: &str = "one-way-file-transfer"; @@ -2280,6 +2290,7 @@ pub mod keys { OPTION_CUSTOM_IMAGE_QUALITY, OPTION_CUSTOM_FPS, OPTION_CODEC_PREFERENCE, + OPTION_SYNC_INIT_CLIPBOARD, ]; // DEFAULT_LOCAL_SETTINGS, OVERWRITE_LOCAL_SETTINGS pub const KEYS_LOCAL_SETTINGS: &[&str] = &[ @@ -2367,7 +2378,6 @@ pub mod keys { OPTION_DEFAULT_CONNECT_PASSWORD, OPTION_HIDE_TRAY, OPTION_ONE_WAY_CLIPBOARD_REDIRECTION, - OPTION_ENABLE_CLIPBOARD_INIT_SYNC, OPTION_ALLOW_LOGON_SCREEN_PASSWORD, OPTION_ONE_WAY_FILE_TRANSFER, ]; diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index c23c967b5809..84d8a897cf58 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -1196,15 +1196,12 @@ impl Remote { } #[cfg(not(any(target_os = "android", target_os = "ios")))] - if let Some(msg_out) = crate::clipboard::get_current_clipboard_msg( - &peer_version, - &peer_platform, - crate::clipboard::ClipboardSide::Client, - ) { - if crate::get_builtin_option( - config::keys::OPTION_ENABLE_CLIPBOARD_INIT_SYNC, - ) != "N" - { + if self.handler.lc.read().unwrap().sync_init_clipboard.v { + if let Some(msg_out) = crate::clipboard::get_current_clipboard_msg( + &peer_version, + &peer_platform, + crate::clipboard::ClipboardSide::Client, + ) { let sender = self.sender.clone(); let permission_config = self.handler.get_permission_config(); tokio::spawn(async move { From a516f01feb982202b95bc291e4cbd593eb5bd0be Mon Sep 17 00:00:00 2001 From: Dmytro <67293930+Nollasko@users.noreply.github.com> Date: Fri, 20 Sep 2024 16:38:16 +0300 Subject: [PATCH 215/541] Update uk.rs (#9416) --- src/lang/uk.rs | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/lang/uk.rs b/src/lang/uk.rs index aa34887be371..41634facc398 100644 --- a/src/lang/uk.rs +++ b/src/lang/uk.rs @@ -246,7 +246,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Paste", "Вставити"), ("Paste here?", "Вставити сюди?"), ("Are you sure to close the connection?", "Ви впевнені, що хочете завершити підключення?"), - ("Download new version", "Завантажити нову версію"), + ("Download new version", "Завантажте нову версію"), ("Touch mode", "Сенсорний режим"), ("Mouse mode", "Режим миші"), ("One-Finger Tap", "Дотик одним пальцем"), @@ -636,16 +636,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Requires at least {} characters", "Потрібно щонайменше {} символів"), ("Wrong PIN", "Неправильний PIN-код"), ("Set PIN", "Встановити PIN-код"), - ("Enable trusted devices", ""), - ("Manage trusted devices", ""), - ("Platform", ""), - ("Days remaining", ""), - ("enable-trusted-devices-tip", ""), - ("Parent directory", ""), - ("Resume", ""), - ("Invalid file name", ""), - ("one-way-file-transfer-tip", ""), - ("Authentication Required", ""), - ("Authenticate", ""), + ("Enable trusted devices", "Увімкнути довірені пристрої"), + ("Manage trusted devices", "Керувати довіреними пристроями"), + ("Platform", "Платформа"), + ("Days remaining", "Залишилося днів"), + ("enable-trusted-devices-tip", "Дозволити довіреним пристроям пропускати двофакторну автентифікацію?"), + ("Parent directory", "Батьківський каталог"), + ("Resume", "Продовжити"), + ("Invalid file name", "Неправильне ім'я файлу"), + ("one-way-file-transfer-tip", "На керованій стороні ввімкнено одностороннє передавання файлів."), + ("Authentication Required", "Потрібна автентифікація"), + ("Authenticate", "Автентифікувати"), ].iter().cloned().collect(); } From 3db55a718c2b89f08a7629d98f6ee54fb726aad6 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Sat, 21 Sep 2024 11:22:33 +0800 Subject: [PATCH 216/541] build 48 --- flutter/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index 5d49249d49c9..899a8bbecf13 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # 1.1.9-1 works for android, but for ios it becomes 1.1.91, need to set it to 1.1.9-a.1 for iOS, will get 1.1.9.1, but iOS store not allow 4 numbers -version: 1.3.1+47 +version: 1.3.1+48 environment: sdk: '^3.1.0' From 1d799483d7460f86d01ec420f2bc7fb55686fdfa Mon Sep 17 00:00:00 2001 From: Alex Rijckaert Date: Sun, 22 Sep 2024 13:06:33 +0200 Subject: [PATCH 217/541] Update nl.rs (#9422) --- src/lang/nl.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lang/nl.rs b/src/lang/nl.rs index 5b2eee79e92a..6a0fa3e702a1 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -644,8 +644,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Parent directory", "Hoofdmap"), ("Resume", "Hervatten"), ("Invalid file name", "Ongeldige bestandsnaam"), - ("one-way-file-transfer-tip", ""), - ("Authentication Required", ""), - ("Authenticate", ""), + ("one-way-file-transfer-tip", "Eenzijdige bestandsoverdracht is ingeschakeld aan de gecontroleerde kant."), + ("Authentication Required", "Verificatie vereist"), + ("Authenticate", "Verificatie"), ].iter().cloned().collect(); } From 5f52ce2c1b780f11a3476e6d7332fe99db7f2ed2 Mon Sep 17 00:00:00 2001 From: solokot Date: Sun, 22 Sep 2024 14:06:45 +0300 Subject: [PATCH 218/541] Update ru.rs (#9421) --- src/lang/ru.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 9235384f43fc..ba8d33f95db2 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -645,7 +645,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Resume", "Продолжить"), ("Invalid file name", "Неправильное имя файла"), ("one-way-file-transfer-tip", "На управляемой стороне включена односторонняя передача файлов."), - ("Authentication Required", ""), - ("Authenticate", ""), + ("Authentication Required", "Требуется аутентификация"), + ("Authenticate", "Аутентификация"), ].iter().cloned().collect(); } From d98f9478243def9c959c6a244750535aa97f9208 Mon Sep 17 00:00:00 2001 From: XLion Date: Mon, 23 Sep 2024 09:17:13 +0800 Subject: [PATCH 219/541] Update translation (#9426) add translation for tw.rs and fix typo on cn.rs --- src/lang/cn.rs | 2 +- src/lang/tw.rs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 887a7297114b..99b3284cbc6b 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -644,7 +644,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Parent directory", "父目录"), ("Resume", "继续"), ("Invalid file name", "无效文件名"), - ("one-way-file-transfer-tip", "被控端启用了单项文件传输"), + ("one-way-file-transfer-tip", "被控端启用了单向文件传输"), ("Authentication Required", "需要身份验证"), ("Authenticate", "认证"), ].iter().cloned().collect(); diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 812d5f8616e6..0a3f291ac974 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -644,8 +644,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Parent directory", "父目錄"), ("Resume", "繼續"), ("Invalid file name", "無效文件名"), - ("one-way-file-transfer-tip", ""), - ("Authentication Required", ""), - ("Authenticate", ""), + ("one-way-file-transfer-tip", "被控端啟用了單向文件傳輸"), + ("Authentication Required", "需要身分驗證"), + ("Authenticate", "認證"), ].iter().cloned().collect(); } From 8fefd34c15a9b5647a10b9bddf2639e71de8b62a Mon Sep 17 00:00:00 2001 From: 21pages Date: Mon, 23 Sep 2024 09:18:22 +0800 Subject: [PATCH 220/541] update web (#9427) Signed-off-by: 21pages --- flutter/lib/common/widgets/overlay.dart | 3 +-- flutter/lib/web/bridge.dart | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/flutter/lib/common/widgets/overlay.dart b/flutter/lib/common/widgets/overlay.dart index a1620b106e36..9b20136e1505 100644 --- a/flutter/lib/common/widgets/overlay.dart +++ b/flutter/lib/common/widgets/overlay.dart @@ -595,8 +595,7 @@ class QualityMonitor extends StatelessWidget { "${qualityMonitorModel.data.targetBitrate ?? '-'}kb"), _row( "Codec", qualityMonitorModel.data.codecFormat ?? '-'), - if (!isWeb) - _row("Chroma", qualityMonitorModel.data.chroma ?? '-'), + _row("Chroma", qualityMonitorModel.data.chroma ?? '-'), ], ), ) diff --git a/flutter/lib/web/bridge.dart b/flutter/lib/web/bridge.dart index 26e94ff1c589..c952c55d27e2 100644 --- a/flutter/lib/web/bridge.dart +++ b/flutter/lib/web/bridge.dart @@ -472,7 +472,7 @@ class RustdeskImpl { required String name, required String value, dynamic hint}) { - return Future(() => js.context.callMethod('SetByName', [ + return Future(() => js.context.callMethod('setByName', [ 'option:session', jsonEncode({'name': name, 'value': value}) ])); From f3f3bb538f112268706c72b109ff7d8200baa702 Mon Sep 17 00:00:00 2001 From: Vasyl Gello Date: Mon, 23 Sep 2024 08:43:59 +0000 Subject: [PATCH 221/541] Fix F-Droid build and bump Android NDK (#9428) * Fix F-Droid build Signed-off-by: Vasyl Gello * Bump Android NDK to r27b Signed-off-by: Vasyl Gello --------- Signed-off-by: Vasyl Gello --- .github/workflows/flutter-build.yml | 2 +- flutter/build_fdroid.sh | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/flutter-build.yml b/.github/workflows/flutter-build.yml index b5f8707874e8..e3a791ea1412 100644 --- a/.github/workflows/flutter-build.yml +++ b/.github/workflows/flutter-build.yml @@ -34,7 +34,7 @@ env: # vcpkg version: 2024.07.12 VCPKG_COMMIT_ID: "1de2026f28ead93ff1773e6e680387643e914ea1" VERSION: "1.3.1" - NDK_VERSION: "r27" + NDK_VERSION: "r27b" #signing keys env variable checks ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}" MACOS_P12_BASE64: "${{ secrets.MACOS_P12_BASE64 }}" diff --git a/flutter/build_fdroid.sh b/flutter/build_fdroid.sh index 2e0a20b6db60..7f3a9cc48f4a 100755 --- a/flutter/build_fdroid.sh +++ b/flutter/build_fdroid.sh @@ -302,6 +302,7 @@ prebuild) sed \ -i \ + -e 's/extended_text: .*/extended_text: 11.1.0/' \ -e 's/uni_links_desktop/#uni_links_desktop/g' \ flutter/pubspec.yaml From f5354069628bfd756f70d8850a016242b09edd80 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Mon, 23 Sep 2024 16:58:21 +0800 Subject: [PATCH 222/541] refact: web, keyboard, translate mode (#9432) Signed-off-by: fufesou --- flutter/lib/models/input_model.dart | 16 ++++++++++++---- flutter/lib/web/bridge.dart | 24 +++++++++++++++++------- flutter/lib/web/common.dart | 11 ++++++----- 3 files changed, 35 insertions(+), 16 deletions(-) diff --git a/flutter/lib/models/input_model.dart b/flutter/lib/models/input_model.dart index ac78e17309b8..b57048a08e4c 100644 --- a/flutter/lib/models/input_model.dart +++ b/flutter/lib/models/input_model.dart @@ -469,8 +469,12 @@ class InputModel { KeyEventResult handleRawKeyEvent(RawKeyEvent e) { if (isViewOnly) return KeyEventResult.handled; - if ((isDesktop || isWebDesktop) && !isInputSourceFlutter) { - return KeyEventResult.handled; + if (!isInputSourceFlutter) { + if (isDesktop) { + return KeyEventResult.handled; + } else if (isWeb) { + return KeyEventResult.ignored; + } } final key = e.logicalKey; @@ -519,8 +523,12 @@ class InputModel { KeyEventResult handleKeyEvent(KeyEvent e) { if (isViewOnly) return KeyEventResult.handled; - if ((isDesktop || isWebDesktop) && !isInputSourceFlutter) { - return KeyEventResult.handled; + if (!isInputSourceFlutter) { + if (isDesktop) { + return KeyEventResult.handled; + } else if (isWeb) { + return KeyEventResult.ignored; + } } if (isWindows || isLinux) { // Ignore meta keys. Because flutter window will loose focus if meta key is pressed. diff --git a/flutter/lib/web/bridge.dart b/flutter/lib/web/bridge.dart index c952c55d27e2..d28e0c26abf7 100644 --- a/flutter/lib/web/bridge.dart +++ b/flutter/lib/web/bridge.dart @@ -352,7 +352,11 @@ class RustdeskImpl { bool sessionIsKeyboardModeSupported( {required UuidValue sessionId, required String mode, dynamic hint}) { - return [kKeyLegacyMode, kKeyMapMode].contains(mode); + if (mainGetInputSource(hint: hint) == 'Input source 1') { + return [kKeyMapMode, kKeyTranslateMode].contains(mode); + } else { + return [kKeyLegacyMode, kKeyMapMode].contains(mode); + } } bool sessionIsMultiUiSession({required UuidValue sessionId, dynamic hint}) { @@ -429,7 +433,7 @@ class RustdeskImpl { void sessionEnterOrLeave( {required UuidValue sessionId, required bool enter, dynamic hint}) { - throw UnimplementedError(); + js.context.callMethod('setByName', ['enter_or_leave', enter]); } Future sessionInputKey( @@ -846,16 +850,21 @@ class RustdeskImpl { } String mainGetInputSource({dynamic hint}) { - // // rdev grab mode - // const CONFIG_INPUT_SOURCE_1 = "Input source 1"; + final inputSource = + js.context.callMethod('getByName', ['option:local', 'input-source']); + // // js grab mode + // export const CONFIG_INPUT_SOURCE_1 = "Input source 1"; // // flutter grab mode - // const CONFIG_INPUT_SOURCE_2 = "Input source 2"; - return 'Input source 2'; + // export const CONFIG_INPUT_SOURCE_2 = "Input source 2"; + return inputSource != '' ? inputSource : 'Input source 1'; } Future mainSetInputSource( {required UuidValue sessionId, required String value, dynamic hint}) { - return Future.value(); + return Future(() => js.context.callMethod('setByName', [ + 'option:local', + jsonEncode({'name': 'input-source', 'value': value}) + ])); } Future mainGetMyId({dynamic hint}) { @@ -1610,6 +1619,7 @@ class RustdeskImpl { String mainSupportedInputSource({dynamic hint}) { return jsonEncode([ + ['Input source 1', 'input_source_1_tip'], ['Input source 2', 'input_source_2_tip'] ]); } diff --git a/flutter/lib/web/common.dart b/flutter/lib/web/common.dart index 0f3a996816d5..4d539d5d47cc 100644 --- a/flutter/lib/web/common.dart +++ b/flutter/lib/web/common.dart @@ -1,5 +1,7 @@ import 'dart:js' as js; import 'dart:html' as html; +// cycle imports, maybe we can improve this +import 'package:flutter_hbb/consts.dart'; final isAndroid_ = false; final isIOS_ = false; @@ -13,8 +15,7 @@ final isDesktop_ = false; String get screenInfo_ => js.context.callMethod('getByName', ['screen_info']); -final _userAgent = html.window.navigator.userAgent.toLowerCase(); - -final isWebOnWindows_ = _userAgent.contains('win'); -final isWebOnLinux_ = _userAgent.contains('linux'); -final isWebOnMacOS_ = _userAgent.contains('mac'); +final _localOs = js.context.callMethod('getByName', ['local_os', '']); +final isWebOnWindows_ = _localOs == kPeerPlatformWindows; +final isWebOnLinux_ = _localOs == kPeerPlatformLinux; +final isWebOnMacOS_ = _localOs == kPeerPlatformMacOS; From 75a14fea23307ed1f3d8df5d447bd47fcea03b45 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Mon, 23 Sep 2024 17:31:37 +0800 Subject: [PATCH 223/541] fix: keyboard, change mode, await, on input source changed (#9434) Signed-off-by: fufesou --- flutter/lib/models/model.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 084ac1b43a68..899affa62bea 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -839,7 +839,7 @@ class FfiModel with ChangeNotifier { for (final mode in [kKeyMapMode, kKeyLegacyMode]) { if (bind.sessionIsKeyboardModeSupported( sessionId: sessionId, mode: mode)) { - bind.sessionSetKeyboardMode(sessionId: sessionId, value: mode); + await bind.sessionSetKeyboardMode(sessionId: sessionId, value: mode); break; } } From 49989e34e44fdda580fe3f453aa2eff7f6bcd6ca Mon Sep 17 00:00:00 2001 From: Kleofass <4000163+Kleofass@users.noreply.github.com> Date: Tue, 24 Sep 2024 04:51:58 +0300 Subject: [PATCH 224/541] Update lv.rs (#9437) --- src/lang/lv.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lang/lv.rs b/src/lang/lv.rs index a89590bf4f15..63a4a7dae4c8 100644 --- a/src/lang/lv.rs +++ b/src/lang/lv.rs @@ -644,8 +644,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Parent directory", "Vecākdirektorijs"), ("Resume", "Atsākt"), ("Invalid file name", "Nederīgs faila nosaukums"), - ("one-way-file-transfer-tip", ""), - ("Authentication Required", ""), - ("Authenticate", ""), + ("one-way-file-transfer-tip", "Kontrolējamajā pusē ir iespējota vienvirziena failu pārsūtīšana."), + ("Authentication Required", "Nepieciešama autentifikācija"), + ("Authenticate", "Autentificēt"), ].iter().cloned().collect(); } From e4f7e126e541cdc5cef4cb8e130783158fa574c4 Mon Sep 17 00:00:00 2001 From: 21pages Date: Tue, 24 Sep 2024 11:37:30 +0800 Subject: [PATCH 225/541] fix check update (#9444) check_software_update runs in a new thread, won't return directly Signed-off-by: 21pages --- flutter/lib/consts.dart | 2 ++ flutter/lib/desktop/pages/desktop_home_page.dart | 16 ++++++++++++++-- flutter/lib/mobile/pages/connection_page.dart | 16 ++++++++++++++-- src/common.rs | 10 ++++++++++ 4 files changed, 40 insertions(+), 4 deletions(-) diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index edc7f427853e..17d5cec271b8 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -570,3 +570,5 @@ enum WindowsTarget { extension WindowsTargetExt on int { WindowsTarget get windowsVersion => getWindowsTarget(this); } + +const kCheckSoftwareUpdateFinish = 'check_software_update_finish'; \ No newline at end of file diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index dee990af4c2d..90fa67dedde1 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -664,9 +664,17 @@ class _DesktopHomePageState extends State void initState() { super.initState(); if (!bind.isCustomClient()) { + platformFFI.registerEventHandler( + kCheckSoftwareUpdateFinish, kCheckSoftwareUpdateFinish, + (Map evt) async { + if (evt['url'] is String) { + setState(() { + updateUrl = evt['url']; + }); + } + }); Timer(const Duration(seconds: 1), () async { - updateUrl = await bind.mainGetSoftwareUpdateUrl(); - if (updateUrl.isNotEmpty) setState(() {}); + bind.mainGetSoftwareUpdateUrl(); }); } _updateTimer = periodic_immediate(const Duration(seconds: 1), () async { @@ -824,6 +832,10 @@ class _DesktopHomePageState extends State _uniLinksSubscription?.cancel(); Get.delete(tag: 'stop-service'); _updateTimer?.cancel(); + if (!bind.isCustomClient()) { + platformFFI.unregisterEventHandler( + kCheckSoftwareUpdateFinish, kCheckSoftwareUpdateFinish); + } super.dispose(); } diff --git a/flutter/lib/mobile/pages/connection_page.dart b/flutter/lib/mobile/pages/connection_page.dart index c6e812389264..181f36e58cc6 100644 --- a/flutter/lib/mobile/pages/connection_page.dart +++ b/flutter/lib/mobile/pages/connection_page.dart @@ -70,9 +70,17 @@ class _ConnectionPageState extends State { } if (isAndroid) { if (!bind.isCustomClient()) { + platformFFI.registerEventHandler( + kCheckSoftwareUpdateFinish, kCheckSoftwareUpdateFinish, + (Map evt) async { + if (evt['url'] is String) { + setState(() { + _updateUrl = evt['url']; + }); + } + }); Timer(const Duration(seconds: 1), () async { - _updateUrl = await bind.mainGetSoftwareUpdateUrl(); - if (_updateUrl.isNotEmpty) setState(() {}); + bind.mainGetSoftwareUpdateUrl(); }); } } @@ -353,6 +361,10 @@ class _ConnectionPageState extends State { if (Get.isRegistered()) { Get.delete(); } + if (!bind.isCustomClient()) { + platformFFI.unregisterEventHandler( + kCheckSoftwareUpdateFinish, kCheckSoftwareUpdateFinish); + } super.dispose(); } } diff --git a/src/common.rs b/src/common.rs index 9950850708f5..c02aea45e8ce 100644 --- a/src/common.rs +++ b/src/common.rs @@ -830,6 +830,16 @@ async fn check_software_update_() -> hbb_common::ResultType<()> { if get_version_number(&latest_release_version) > get_version_number(crate::VERSION) { *SOFTWARE_UPDATE_URL.lock().unwrap() = response_url; } + #[cfg(feature = "flutter")] + { + let mut m = HashMap::new(); + m.insert("name", "check_software_update_finish"); + let url = SOFTWARE_UPDATE_URL.lock().unwrap().clone(); + m.insert("url", url.as_str()); + if let Ok(data) = serde_json::to_string(&m) { + let _ = crate::flutter::push_global_event(crate::flutter::APP_TYPE_MAIN, data); + } + } Ok(()) } From 664a3e186eaf189ec3f49241552e2c71c11c6ac2 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Tue, 24 Sep 2024 12:00:37 +0800 Subject: [PATCH 226/541] clean SOFTWARE_UPDATE_URL --- flutter/lib/web/bridge.dart | 2 +- src/common.rs | 19 +++++++++---------- src/flutter_ffi.rs | 4 +--- src/main.rs | 2 -- 4 files changed, 11 insertions(+), 16 deletions(-) diff --git a/flutter/lib/web/bridge.dart b/flutter/lib/web/bridge.dart index d28e0c26abf7..c99ef140da4c 100644 --- a/flutter/lib/web/bridge.dart +++ b/flutter/lib/web/bridge.dart @@ -1063,7 +1063,7 @@ class RustdeskImpl { () => js.context.callMethod('getByName', ['option', 'last_remote_id'])); } - Future mainGetSoftwareUpdateUrl({dynamic hint}) { + Future mainGetSoftwareUpdateUrl({dynamic hint}) { throw UnimplementedError(); } diff --git a/src/common.rs b/src/common.rs index c02aea45e8ce..f53dd703fe6a 100644 --- a/src/common.rs +++ b/src/common.rs @@ -828,17 +828,16 @@ async fn check_software_update_() -> hbb_common::ResultType<()> { let response_url = latest_release_response.url().to_string(); if get_version_number(&latest_release_version) > get_version_number(crate::VERSION) { - *SOFTWARE_UPDATE_URL.lock().unwrap() = response_url; - } - #[cfg(feature = "flutter")] - { - let mut m = HashMap::new(); - m.insert("name", "check_software_update_finish"); - let url = SOFTWARE_UPDATE_URL.lock().unwrap().clone(); - m.insert("url", url.as_str()); - if let Ok(data) = serde_json::to_string(&m) { - let _ = crate::flutter::push_global_event(crate::flutter::APP_TYPE_MAIN, data); + #[cfg(feature = "flutter")] + { + let mut m = HashMap::new(); + m.insert("name", "check_software_update_finish"); + m.insert("url", &response_url); + if let Ok(data) = serde_json::to_string(&m) { + let _ = crate::flutter::push_global_event(crate::flutter::APP_TYPE_MAIN, data); + } } + *SOFTWARE_UPDATE_URL.lock().unwrap() = response_url; } Ok(()) } diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 72b0e5b37b42..020bc98aa920 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -61,7 +61,6 @@ fn initialize(app_dir: &str, custom_client_config: &str) { scrap::mediacodec::check_mediacodec(); crate::common::test_rendezvous_server(); crate::common::test_nat_type(); - crate::common::check_software_update(); } #[cfg(target_os = "ios")] { @@ -1376,11 +1375,10 @@ pub fn main_get_last_remote_id() -> String { LocalConfig::get_remote_id() } -pub fn main_get_software_update_url() -> String { +pub fn main_get_software_update_url() { if get_local_option("enable-check-update".to_string()) != "N" { crate::common::check_software_update(); } - crate::common::SOFTWARE_UPDATE_URL.lock().unwrap().clone() } pub fn main_get_home_dir() -> String { diff --git a/src/main.rs b/src/main.rs index 44ace8a76e56..f295363aa90e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,8 +12,6 @@ fn main() { } common::test_rendezvous_server(); common::test_nat_type(); - #[cfg(target_os = "android")] - crate::common::check_software_update(); common::global_clean(); } From e0095aebdaf7610b164886e505f965be67183ff8 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Tue, 24 Sep 2024 13:53:17 +0800 Subject: [PATCH 227/541] refact: web elevation (#9445) Signed-off-by: fufesou --- flutter/lib/web/bridge.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/flutter/lib/web/bridge.dart b/flutter/lib/web/bridge.dart index c99ef140da4c..6e758dae438c 100644 --- a/flutter/lib/web/bridge.dart +++ b/flutter/lib/web/bridge.dart @@ -608,7 +608,7 @@ class RustdeskImpl { Future sessionElevateDirect( {required UuidValue sessionId, dynamic hint}) { - throw UnimplementedError(); + return Future(() => js.context.callMethod('setByName', ['elevate_direct'])); } Future sessionElevateWithLogon( @@ -618,7 +618,7 @@ class RustdeskImpl { dynamic hint}) { return Future(() => js.context.callMethod('setByName', [ 'elevate_with_logon', - jsonEncode({username, password}) + jsonEncode({'username': username, 'password': password}) ])); } From ba88bc9e8bdf14ea44e371315eb9cb557768f16c Mon Sep 17 00:00:00 2001 From: bovirus <1262554+bovirus@users.noreply.github.com> Date: Tue, 24 Sep 2024 14:33:28 +0200 Subject: [PATCH 228/541] Update Italian language (#9452) --- src/lang/it.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/lang/it.rs b/src/lang/it.rs index 008fb3b3518f..a07df2da9227 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -369,7 +369,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop session recording", "Ferma registrazione sessione"), ("Enable recording session", "Abilita registrazione sessione"), ("Enable LAN discovery", "Abilita rilevamento LAN"), - ("Deny LAN discovery", "Nega rilevamento LAN"), + ("Deny LAN discovery", "Disabilita rilevamento LAN"), ("Write a message", "Scrivi un messaggio"), ("Prompt", "Richiedi"), ("Please wait for confirmation of UAC...", "Attendi la conferma dell'UAC..."), @@ -532,7 +532,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("HSV Color", "Colore HSV"), ("Installation Successful!", "Installazione completata"), ("Installation failed!", "Installazione fallita"), - ("Reverse mouse wheel", "Rotella mouse inversa"), + ("Reverse mouse wheel", "Funzione rotellina mouse inversa"), ("{} sessions", "{} sessioni"), ("scam_title", "Potresti essere stato TRUFFATO!"), ("scam_text1", "Se sei al telefono con qualcuno che NON conosci NON DI TUA FIDUCIA che ti ha chiesto di usare RustDesk e di avviare il servizio, non procedere e riattacca subito."), @@ -632,7 +632,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About RustDesk", "Info su RustDesk"), ("Send clipboard keystrokes", "Invia sequenze tasti appunti"), ("network_error_tip", "Controlla la connessione di rete, quindi seleziona 'Riprova'."), - ("Unlock with PIN", "Sblocca con PIN"), + ("Unlock with PIN", "Abilita sblocco con PIN"), ("Requires at least {} characters", "Richiede almeno {} caratteri"), ("Wrong PIN", "PIN errato"), ("Set PIN", "Imposta PIN"), @@ -644,7 +644,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Parent directory", "Cartella principale"), ("Resume", "Riprendi"), ("Invalid file name", "Nome file non valido"), - ("one-way-file-transfer-tip", "Il trasferimento file unidirezionale è abilitato sul lato controllato."), + ("one-way-file-transfer-tip", "Sul lato controllato è abilitato il trasferimento file unidirezionale."), ("Authentication Required", "Richiesta autenticazione"), ("Authenticate", "Autentica"), ].iter().cloned().collect(); From ce5151032ef8d3245b1fbbc2754d577f38434c17 Mon Sep 17 00:00:00 2001 From: Lumiphare <58658634+Lumiphare@users.noreply.github.com> Date: Wed, 25 Sep 2024 08:48:45 +0800 Subject: [PATCH 229/541] Update README-ZH.md (#9457) * Update CONTRIBUTING.md links to point to the Chinese version * translated with AI assistance and manual refinement * Adapted from the official Chinese translation of the Contributor Covenant * Improve README-ZH.md --------- Co-authored-by: sea Co-authored-by: Lumiphare --- docs/README-ZH.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/docs/README-ZH.md b/docs/README-ZH.md index 0460384ab42f..5a5f56b204e0 100644 --- a/docs/README-ZH.md +++ b/docs/README-ZH.md @@ -8,7 +8,7 @@ [English] | [Українська] | [česky] | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt] | [Ελληνικά]

    -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) @@ -32,7 +32,9 @@ RustDesk 期待各位的贡献. 如何参与开发? 详情请看 [CONTRIBUTING-Z ## 依赖 -桌面版本界面使用[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) | [Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) | @@ -207,12 +209,13 @@ target/release/rustdesk - **[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/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/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/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代码 ## 截图 From 7c55e3266b1cf13a92e6973323aba3034651afb1 Mon Sep 17 00:00:00 2001 From: 21pages Date: Wed, 25 Sep 2024 15:11:11 +0800 Subject: [PATCH 230/541] fix peers view ChangeNotifierProvider update (#9459) Signed-off-by: 21pages --- flutter/lib/common/widgets/peers_view.dart | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/flutter/lib/common/widgets/peers_view.dart b/flutter/lib/common/widgets/peers_view.dart index b18de82d995c..e80e5ffe343b 100644 --- a/flutter/lib/common/widgets/peers_view.dart +++ b/flutter/lib/common/widgets/peers_view.dart @@ -128,8 +128,9 @@ class _PeersViewState extends State<_PeersView> // // 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)) { + if (isWindows && + DateTime.now().difference(_lastWindowRestoreTime) < + const Duration(milliseconds: 300)) { return; } _queryCount = _maxQueryCount; @@ -170,8 +171,9 @@ class _PeersViewState extends State<_PeersView> // We should avoid too many rebuilds. MacOS(m1, 14.6.1) on Flutter 3.19.6. // Continious rebuilds of `ChangeNotifierProvider` will cause memory leak. // Simple demo can reproduce this issue. - return ChangeNotifierProvider( - create: (context) => widget.peers, + return ChangeNotifierProvider.value( + // https://pub.dev/packages/provider: If you already have an object instance and want to expose it, it would be best to use the .value constructor of a provider. + value: widget.peers, child: Consumer(builder: (context, peers, child) { if (peers.peers.isEmpty) { gFFI.peerTabModel.setCurrentTabCachedPeers([]); @@ -186,7 +188,7 @@ class _PeersViewState extends State<_PeersView> ).paddingOnly(bottom: 10), Text( translate( - _emptyMessages[widget.peers.loadEvent] ?? 'Empty', + _emptyMessages[peers.loadEvent] ?? 'Empty', ), textAlign: TextAlign.center, style: TextStyle( From 1d6873f6223deb85aec48f3b34685fb9e8abe963 Mon Sep 17 00:00:00 2001 From: flusheDData <116861809+flusheDData@users.noreply.github.com> Date: Thu, 26 Sep 2024 03:29:49 +0200 Subject: [PATCH 231/541] New translation terms (#9463) * Update es.rs New terms added * Update es.rs New terms added --- src/lang/es.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lang/es.rs b/src/lang/es.rs index e42a5abed84e..cb5e4a25b397 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -644,8 +644,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Parent directory", "Directorio superior"), ("Resume", "Continuar"), ("Invalid file name", "Nombre de archivo no válido"), - ("one-way-file-transfer-tip", ""), - ("Authentication Required", ""), - ("Authenticate", ""), + ("one-way-file-transfer-tip", "La transferencia en un sentido está habilitada en el lado controlado."), + ("Authentication Required", "Se requiere autenticación"), + ("Authenticate", "Autenticar"), ].iter().cloned().collect(); } From 6d8b5b289f8642446b94dac11cd4db2d9b8d0304 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A1ssio=20Oliveira?= <57467189+zkassimz@users.noreply.github.com> Date: Wed, 25 Sep 2024 23:34:12 -0300 Subject: [PATCH 232/541] Refactor ScanPage for better performance and memory management (#9464) - Added null checks in `reassemble` method to avoid potential null pointer exceptions when pausing/resuming the camera. - Refactored image picking and QR code decoding process to use async/await, avoiding UI blocking with synchronous file reads. - Improved exception handling by making it more specific to QR code reading errors. - Introduced `StreamSubscription` for the QR scan listener and ensured proper cancellation in `dispose` method to prevent memory leaks. - Separated button building logic (`_buildImagePickerButton`, `_buildFlashToggleButton`, `_buildCameraSwitchButton`) to enhance code readability and maintainability. --- flutter/lib/mobile/pages/scan_page.dart | 151 +++++++++++++----------- 1 file changed, 81 insertions(+), 70 deletions(-) diff --git a/flutter/lib/mobile/pages/scan_page.dart b/flutter/lib/mobile/pages/scan_page.dart index 527e7446ff20..e92400dba6da 100644 --- a/flutter/lib/mobile/pages/scan_page.dart +++ b/flutter/lib/mobile/pages/scan_page.dart @@ -19,95 +19,48 @@ class ScanPage extends StatefulWidget { class _ScanPageState extends State { QRViewController? controller; final GlobalKey qrKey = GlobalKey(debugLabel: 'QR'); + StreamSubscription? scanSubscription; - // In order to get hot reload to work we need to pause the camera if the platform - // is android, or resume the camera if the platform is iOS. @override void reassemble() { super.reassemble(); - if (isAndroid) { + if (isAndroid && controller != null) { controller!.pauseCamera(); + } else if (controller != null) { + controller!.resumeCamera(); } - controller!.resumeCamera(); } @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar( - title: const Text('Scan QR'), - actions: [ - IconButton( - color: Colors.white, - icon: Icon(Icons.image_search), - iconSize: 32.0, - onPressed: () async { - final ImagePicker picker = ImagePicker(); - final XFile? file = - await picker.pickImage(source: ImageSource.gallery); - if (file != null) { - var image = img.decodeNamedImage( - file.path, File(file.path).readAsBytesSync())!; - - LuminanceSource source = RGBLuminanceSource( - image.width, - image.height, - image - .getBytes(order: img.ChannelOrder.abgr) - .buffer - .asInt32List()); - var bitmap = BinaryBitmap(HybridBinarizer(source)); - - var reader = QRCodeReader(); - try { - var result = reader.decode(bitmap); - if (result.text.startsWith(bind.mainUriPrefixSync())) { - handleUriLink(uriString: result.text); - } else { - showServerSettingFromQr(result.text); - } - } catch (e) { - showToast('No QR code found'); - } - } - }), - IconButton( - color: Colors.yellow, - icon: Icon(Icons.flash_on), - iconSize: 32.0, - onPressed: () async { - await controller?.toggleFlash(); - }), - IconButton( - color: Colors.white, - icon: Icon(Icons.switch_camera), - iconSize: 32.0, - onPressed: () async { - await controller?.flipCamera(); - }, - ), - ], - ), - body: _buildQrView(context)); + appBar: AppBar( + title: const Text('Scan QR'), + actions: [ + _buildImagePickerButton(), + _buildFlashToggleButton(), + _buildCameraSwitchButton(), + ], + ), + body: _buildQrView(context), + ); } Widget _buildQrView(BuildContext context) { - // For this example we check how width or tall the device is and change the scanArea and overlay accordingly. - var scanArea = (MediaQuery.of(context).size.width < 400 || - MediaQuery.of(context).size.height < 400) + var scanArea = MediaQuery.of(context).size.width < 400 || + MediaQuery.of(context).size.height < 400 ? 150.0 : 300.0; - // To ensure the Scanner view is properly sizes after rotation - // we need to listen for Flutter SizeChanged notification and update controller return QRView( key: qrKey, onQRViewCreated: _onQRViewCreated, overlay: QrScannerOverlayShape( - borderColor: Colors.red, - borderRadius: 10, - borderLength: 30, - borderWidth: 10, - cutOutSize: scanArea), + borderColor: Colors.red, + borderRadius: 10, + borderLength: 30, + borderWidth: 10, + cutOutSize: scanArea, + ), onPermissionSet: (ctrl, p) => _onPermissionSet(context, ctrl, p), ); } @@ -116,7 +69,7 @@ class _ScanPageState extends State { setState(() { this.controller = controller; }); - controller.scannedDataStream.listen((scanData) { + scanSubscription = controller.scannedDataStream.listen((scanData) { if (scanData.code != null) { showServerSettingFromQr(scanData.code!); } @@ -129,8 +82,66 @@ class _ScanPageState extends State { } } + Future _pickImage() async { + final ImagePicker picker = ImagePicker(); + final XFile? file = await picker.pickImage(source: ImageSource.gallery); + if (file != null) { + try { + var image = img.decodeImage(await File(file.path).readAsBytes())!; + LuminanceSource source = RGBLuminanceSource( + image.width, + image.height, + image.getBytes(order: img.ChannelOrder.abgr).buffer.asInt32List(), + ); + var bitmap = BinaryBitmap(HybridBinarizer(source)); + + var reader = QRCodeReader(); + var result = reader.decode(bitmap); + if (result.text.startsWith(bind.mainUriPrefixSync())) { + handleUriLink(uriString: result.text); + } else { + showServerSettingFromQr(result.text); + } + } catch (e) { + showToast('No QR code found'); + } + } + } + + Widget _buildImagePickerButton() { + return IconButton( + color: Colors.white, + icon: Icon(Icons.image_search), + iconSize: 32.0, + onPressed: _pickImage, + ); + } + + Widget _buildFlashToggleButton() { + return IconButton( + color: Colors.yellow, + icon: Icon(Icons.flash_on), + iconSize: 32.0, + onPressed: () async { + await controller?.toggleFlash(); + }, + ); + } + + Widget _buildCameraSwitchButton() { + return IconButton( + color: Colors.white, + icon: Icon(Icons.switch_camera), + iconSize: 32.0, + onPressed: () async { + await controller?.flipCamera(); + }, + ); + } + @override void dispose() { + scanSubscription?.cancel(); controller?.dispose(); super.dispose(); } From c74bdcdfdb2dbb5321ae7a260462bd2e025a90e6 Mon Sep 17 00:00:00 2001 From: RustDesk <71636191+rustdesk@users.noreply.github.com> Date: Thu, 26 Sep 2024 14:20:37 +0800 Subject: [PATCH 233/541] Revert "fix peers view ChangeNotifierProvider update (#9459)" (#9471) This reverts commit 7c55e3266b1cf13a92e6973323aba3034651afb1. --- flutter/lib/common/widgets/peers_view.dart | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/flutter/lib/common/widgets/peers_view.dart b/flutter/lib/common/widgets/peers_view.dart index e80e5ffe343b..b18de82d995c 100644 --- a/flutter/lib/common/widgets/peers_view.dart +++ b/flutter/lib/common/widgets/peers_view.dart @@ -128,9 +128,8 @@ class _PeersViewState extends State<_PeersView> // // 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)) { + if (isWindows && DateTime.now().difference(_lastWindowRestoreTime) < + const Duration(milliseconds: 300)) { return; } _queryCount = _maxQueryCount; @@ -171,9 +170,8 @@ class _PeersViewState extends State<_PeersView> // We should avoid too many rebuilds. MacOS(m1, 14.6.1) on Flutter 3.19.6. // Continious rebuilds of `ChangeNotifierProvider` will cause memory leak. // Simple demo can reproduce this issue. - return ChangeNotifierProvider.value( - // https://pub.dev/packages/provider: If you already have an object instance and want to expose it, it would be best to use the .value constructor of a provider. - value: widget.peers, + return ChangeNotifierProvider( + create: (context) => widget.peers, child: Consumer(builder: (context, peers, child) { if (peers.peers.isEmpty) { gFFI.peerTabModel.setCurrentTabCachedPeers([]); @@ -188,7 +186,7 @@ class _PeersViewState extends State<_PeersView> ).paddingOnly(bottom: 10), Text( translate( - _emptyMessages[peers.loadEvent] ?? 'Empty', + _emptyMessages[widget.peers.loadEvent] ?? 'Empty', ), textAlign: TextAlign.center, style: TextStyle( From ffc73f86a0d75cc3f3c0ac02a133ef8312c77c72 Mon Sep 17 00:00:00 2001 From: 21pages Date: Thu, 26 Sep 2024 22:08:32 +0800 Subject: [PATCH 234/541] fix ab peers view, all peer tab use global peers model (#9475) Use ChangeNotifierProvider.value, and each peer tab has a global unique `Peers` model, then `load peers` and `build peers` will always be the same one. Signed-off-by: 21pages --- flutter/lib/common/widgets/address_book.dart | 109 ++++++++++--------- flutter/lib/common/widgets/my_group.dart | 8 +- flutter/lib/common/widgets/peers_view.dart | 79 +++++++------- flutter/lib/models/ab_model.dart | 6 + flutter/lib/models/group_model.dart | 9 +- flutter/lib/models/model.dart | 15 +++ 6 files changed, 134 insertions(+), 92 deletions(-) diff --git a/flutter/lib/common/widgets/address_book.dart b/flutter/lib/common/widgets/address_book.dart index a0a456807a86..78bd20ef0336 100644 --- a/flutter/lib/common/widgets/address_book.dart +++ b/flutter/lib/common/widgets/address_book.dart @@ -241,14 +241,15 @@ class _AddressBookState extends State { 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), - ]), - )), + 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), @@ -358,7 +359,6 @@ class _AddressBookState extends State { alignment: Alignment.topLeft, child: AddressBookPeersView( menuPadding: widget.menuPadding, - getInitPeers: () => gFFI.abModel.currentAbPeers, )), ); } @@ -509,19 +509,19 @@ class _AddressBookState extends State { 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); + 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)); } @@ -546,23 +546,28 @@ class _AddressBookState extends State { ], ), input: Obx(() => TextField( - controller: idController, - inputFormatters: [IDTextInputFormatter()], - decoration: InputDecoration( - labelText: stateGlobal.isPortrait.isFalse ? null : translate('ID'), - errorText: errorMsg, - errorMaxLines: 5), - ))), + controller: idController, + inputFormatters: [IDTextInputFormatter()], + decoration: InputDecoration( + labelText: stateGlobal.isPortrait.isFalse + ? null + : translate('ID'), + errorText: errorMsg, + errorMaxLines: 5), + ))), row( lable: Text( translate('Alias'), style: style, ), input: Obx(() => TextField( - controller: aliasController, - decoration: InputDecoration( - labelText: stateGlobal.isPortrait.isFalse ? null : translate('Alias'), - ),)), + controller: aliasController, + decoration: InputDecoration( + labelText: stateGlobal.isPortrait.isFalse + ? null + : translate('Alias'), + ), + )), ), if (isCurrentAbShared) row( @@ -570,25 +575,29 @@ class _AddressBookState extends State { translate('Password'), 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; - }); - }, + 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, diff --git a/flutter/lib/common/widgets/my_group.dart b/flutter/lib/common/widgets/my_group.dart index 2d26536eb8f1..867d71dff2da 100644 --- a/flutter/lib/common/widgets/my_group.dart +++ b/flutter/lib/common/widgets/my_group.dart @@ -83,8 +83,8 @@ class _MyGroupState extends State { child: Align( alignment: Alignment.topLeft, child: MyGroupPeerView( - menuPadding: widget.menuPadding, - getInitPeers: () => gFFI.groupModel.peers)), + menuPadding: widget.menuPadding, + )), ) ], ); @@ -115,8 +115,8 @@ class _MyGroupState extends State { child: Align( alignment: Alignment.topLeft, child: MyGroupPeerView( - menuPadding: widget.menuPadding, - getInitPeers: () => gFFI.groupModel.peers)), + menuPadding: widget.menuPadding, + )), ) ], ); diff --git a/flutter/lib/common/widgets/peers_view.dart b/flutter/lib/common/widgets/peers_view.dart index b18de82d995c..32db418f5bba 100644 --- a/flutter/lib/common/widgets/peers_view.dart +++ b/flutter/lib/common/widgets/peers_view.dart @@ -6,6 +6,7 @@ import 'package:flutter/foundation.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/models/peer_tab_model.dart'; import 'package:flutter_hbb/models/state_model.dart'; import 'package:get/get.dart'; import 'package:provider/provider.dart'; @@ -42,6 +43,14 @@ class LoadEvent { 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 final peerSearchText = "".obs; @@ -128,8 +137,9 @@ class _PeersViewState extends State<_PeersView> // // 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)) { + if (isWindows && + DateTime.now().difference(_lastWindowRestoreTime) < + const Duration(milliseconds: 300)) { return; } _queryCount = _maxQueryCount; @@ -170,8 +180,8 @@ class _PeersViewState extends State<_PeersView> // We should avoid too many rebuilds. MacOS(m1, 14.6.1) on Flutter 3.19.6. // Continious rebuilds of `ChangeNotifierProvider` will cause memory leak. // Simple demo can reproduce this issue. - return ChangeNotifierProvider( - create: (context) => widget.peers, + return ChangeNotifierProvider.value( + value: widget.peers, child: Consumer(builder: (context, peers, child) { if (peers.peers.isEmpty) { gFFI.peerTabModel.setCurrentTabCachedPeers([]); @@ -403,28 +413,39 @@ class _PeersViewState extends State<_PeersView> } abstract class BasePeersView extends StatelessWidget { - final String name; - final String loadEvent; + final PeerTabIndex peerTabIndex; final PeerFilter? peerFilter; final PeerCardBuilder peerCardBuilder; - final GetInitPeers? getInitPeers; const BasePeersView({ Key? key, - required this.name, - required this.loadEvent, + required this.peerTabIndex, this.peerFilter, required this.peerCardBuilder, - required this.getInitPeers, }) : super(key: key); @override 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( - peers: - Peers(name: name, loadEvent: loadEvent, getInitPeers: getInitPeers), - peerFilter: peerFilter, - peerCardBuilder: peerCardBuilder); + peers: peers, peerFilter: peerFilter, peerCardBuilder: peerCardBuilder); } } @@ -433,13 +454,11 @@ class RecentPeersView extends BasePeersView { {Key? key, EdgeInsets? menuPadding, ScrollController? scrollController}) : super( key: key, - name: 'recent peer', - loadEvent: LoadEvent.recent, + peerTabIndex: PeerTabIndex.recent, peerCardBuilder: (Peer peer) => RecentPeerCard( peer: peer, menuPadding: menuPadding, ), - getInitPeers: null, ); @override @@ -455,13 +474,11 @@ class FavoritePeersView extends BasePeersView { {Key? key, EdgeInsets? menuPadding, ScrollController? scrollController}) : super( key: key, - name: 'favorite peer', - loadEvent: LoadEvent.favorite, + peerTabIndex: PeerTabIndex.fav, peerCardBuilder: (Peer peer) => FavoritePeerCard( peer: peer, menuPadding: menuPadding, ), - getInitPeers: null, ); @override @@ -477,13 +494,11 @@ class DiscoveredPeersView extends BasePeersView { {Key? key, EdgeInsets? menuPadding, ScrollController? scrollController}) : super( key: key, - name: 'discovered peer', - loadEvent: LoadEvent.lan, + peerTabIndex: PeerTabIndex.lan, peerCardBuilder: (Peer peer) => DiscoveredPeerCard( peer: peer, menuPadding: menuPadding, ), - getInitPeers: null, ); @override @@ -496,21 +511,16 @@ class DiscoveredPeersView extends BasePeersView { class AddressBookPeersView extends BasePeersView { AddressBookPeersView( - {Key? key, - EdgeInsets? menuPadding, - ScrollController? scrollController, - required GetInitPeers getInitPeers}) + {Key? key, EdgeInsets? menuPadding, ScrollController? scrollController}) : super( key: key, - name: 'address book peer', - loadEvent: LoadEvent.addressBook, + peerTabIndex: PeerTabIndex.ab, peerFilter: (Peer peer) => _hitTag(gFFI.abModel.selectedTags, peer.tags), peerCardBuilder: (Peer peer) => AddressBookPeerCard( peer: peer, menuPadding: menuPadding, ), - getInitPeers: getInitPeers, ); static bool _hitTag(List selectedTags, List idents) { @@ -537,20 +547,15 @@ class AddressBookPeersView extends BasePeersView { class MyGroupPeerView extends BasePeersView { MyGroupPeerView( - {Key? key, - EdgeInsets? menuPadding, - ScrollController? scrollController, - required GetInitPeers getInitPeers}) + {Key? key, EdgeInsets? menuPadding, ScrollController? scrollController}) : super( key: key, - name: 'group peer', - loadEvent: LoadEvent.group, + peerTabIndex: PeerTabIndex.group, peerFilter: filter, peerCardBuilder: (Peer peer) => MyGroupPeerCard( peer: peer, menuPadding: menuPadding, ), - getInitPeers: getInitPeers, ); static bool filter(Peer peer) { diff --git a/flutter/lib/models/ab_model.dart b/flutter/lib/models/ab_model.dart index 6f3820e86bb0..0da84e0f26c8 100644 --- a/flutter/lib/models/ab_model.dart +++ b/flutter/lib/models/ab_model.dart @@ -66,10 +66,16 @@ class AbModel { var listInitialized = false; var _maxPeerOneAb = 0; + late final Peers peersModel; + WeakReference parent; AbModel(this.parent) { addressbooks.clear(); + peersModel = Peers( + name: PeersModelName.addressBook, + getInitPeers: () => currentAbPeers, + loadEvent: LoadEvent.addressBook); if (desktopType == DesktopType.main) { Timer.periodic(Duration(milliseconds: 500), (timer) async { if (_timerCounter++ % 6 == 0) { diff --git a/flutter/lib/models/group_model.dart b/flutter/lib/models/group_model.dart index 184c94bfff31..b14ccd46b0e0 100644 --- a/flutter/lib/models/group_model.dart +++ b/flutter/lib/models/group_model.dart @@ -23,7 +23,14 @@ class GroupModel { bool get emtpy => users.isEmpty && peers.isEmpty; - GroupModel(this.parent); + late final Peers peersModel; + + GroupModel(this.parent) { + peersModel = Peers( + name: PeersModelName.group, + getInitPeers: () => peers, + loadEvent: LoadEvent.group); + } Future pull({force = true, quiet = false}) async { if (bind.isDisableGroupPanel()) return; diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 899affa62bea..3f2dcade9ae1 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -7,12 +7,14 @@ import 'dart:ui' as ui; import 'package:desktop_multi_window/desktop_multi_window.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_hbb/common/widgets/peers_view.dart'; import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/models/ab_model.dart'; import 'package:flutter_hbb/models/chat_model.dart'; import 'package:flutter_hbb/models/cm_file_model.dart'; import 'package:flutter_hbb/models/file_model.dart'; import 'package:flutter_hbb/models/group_model.dart'; +import 'package:flutter_hbb/models/peer_model.dart'; import 'package:flutter_hbb/models/peer_tab_model.dart'; import 'package:flutter_hbb/models/server_model.dart'; import 'package:flutter_hbb/models/user_model.dart'; @@ -2397,6 +2399,9 @@ class FFI { late final ElevationModel elevationModel; // session late final CmFileModel cmFileModel; // cm late final TextureModel textureModel; //session + late final Peers recentPeersModel; // global + late final Peers favoritePeersModel; // global + late final Peers lanPeersModel; // global FFI(SessionID? sId) { sessionId = sId ?? (isDesktop ? Uuid().v4obj() : _constSessionId); @@ -2417,6 +2422,16 @@ class FFI { elevationModel = ElevationModel(WeakReference(this)); cmFileModel = CmFileModel(WeakReference(this)); textureModel = TextureModel(WeakReference(this)); + recentPeersModel = Peers( + name: PeersModelName.recent, + loadEvent: LoadEvent.recent, + getInitPeers: null); + favoritePeersModel = Peers( + name: PeersModelName.favorite, + loadEvent: LoadEvent.favorite, + getInitPeers: null); + lanPeersModel = Peers( + name: PeersModelName.lan, loadEvent: LoadEvent.lan, getInitPeers: null); } /// Mobile reuse FFI From d4184fd865be29ce955bd2c95d89b486cb175cba Mon Sep 17 00:00:00 2001 From: rustdesk Date: Thu, 26 Sep 2024 23:07:53 +0800 Subject: [PATCH 235/541] bump to 1.3.2 --- .github/workflows/flutter-build.yml | 2 +- .github/workflows/playground.yml | 2 +- Cargo.lock | 4 ++-- Cargo.toml | 2 +- appimage/AppImageBuilder-aarch64.yml | 2 +- appimage/AppImageBuilder-x86_64.yml | 2 +- flatpak/rustdesk.json | 2 +- flutter/pubspec.yaml | 2 +- libs/portable/Cargo.toml | 2 +- res/PKGBUILD | 2 +- res/rpm-flutter-suse.spec | 2 +- res/rpm-flutter.spec | 2 +- res/rpm.spec | 2 +- 13 files changed, 14 insertions(+), 14 deletions(-) diff --git a/.github/workflows/flutter-build.yml b/.github/workflows/flutter-build.yml index e3a791ea1412..dd21515ba547 100644 --- a/.github/workflows/flutter-build.yml +++ b/.github/workflows/flutter-build.yml @@ -33,7 +33,7 @@ env: VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite" # vcpkg version: 2024.07.12 VCPKG_COMMIT_ID: "1de2026f28ead93ff1773e6e680387643e914ea1" - VERSION: "1.3.1" + VERSION: "1.3.2" NDK_VERSION: "r27b" #signing keys env variable checks ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}" diff --git a/.github/workflows/playground.yml b/.github/workflows/playground.yml index 205ce8f1ed54..efd6974a9914 100644 --- a/.github/workflows/playground.yml +++ b/.github/workflows/playground.yml @@ -18,7 +18,7 @@ env: VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite" # vcpkg version: 2024.06.15 VCPKG_COMMIT_ID: "f7423ee180c4b7f40d43402c2feb3859161ef625" - VERSION: "1.3.1" + VERSION: "1.3.2" NDK_VERSION: "r26d" #signing keys env variable checks ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}" diff --git a/Cargo.lock b/Cargo.lock index 486b7a6f9be1..e1474f2f3a58 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5480,7 +5480,7 @@ dependencies = [ [[package]] name = "rustdesk" -version = "1.3.1" +version = "1.3.2" dependencies = [ "android-wakelock", "android_logger", @@ -5580,7 +5580,7 @@ dependencies = [ [[package]] name = "rustdesk-portable-packer" -version = "1.3.1" +version = "1.3.2" dependencies = [ "brotli", "dirs 5.0.1", diff --git a/Cargo.toml b/Cargo.toml index c71b2918bd27..1ee749afb80e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rustdesk" -version = "1.3.1" +version = "1.3.2" authors = ["rustdesk "] edition = "2021" build= "build.rs" diff --git a/appimage/AppImageBuilder-aarch64.yml b/appimage/AppImageBuilder-aarch64.yml index e23cde7172df..5ff9fc2a7b23 100644 --- a/appimage/AppImageBuilder-aarch64.yml +++ b/appimage/AppImageBuilder-aarch64.yml @@ -18,7 +18,7 @@ AppDir: id: rustdesk name: rustdesk icon: rustdesk - version: 1.3.1 + version: 1.3.2 exec: usr/lib/rustdesk/rustdesk exec_args: $@ apt: diff --git a/appimage/AppImageBuilder-x86_64.yml b/appimage/AppImageBuilder-x86_64.yml index 83df32ec87bc..d8f0991cf953 100644 --- a/appimage/AppImageBuilder-x86_64.yml +++ b/appimage/AppImageBuilder-x86_64.yml @@ -18,7 +18,7 @@ AppDir: id: rustdesk name: rustdesk icon: rustdesk - version: 1.3.1 + version: 1.3.2 exec: usr/lib/rustdesk/rustdesk exec_args: $@ apt: diff --git a/flatpak/rustdesk.json b/flatpak/rustdesk.json index 6d7acb5b89ca..6b205aa99925 100644 --- a/flatpak/rustdesk.json +++ b/flatpak/rustdesk.json @@ -17,7 +17,7 @@ "sources": [ { "type": "archive", - "url": "https://github.com/linux-pam/linux-pam/releases/download/v1.3.1/Linux-PAM-1.3.1.tar.xz", + "url": "https://github.com/linux-pam/linux-pam/releases/download/v1.3.2/Linux-PAM-1.3.2.tar.xz", "sha256": "eff47a4ecd833fbf18de9686632a70ee8d0794b79aecb217ebd0ce11db4cd0db" } ] diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index 899a8bbecf13..cc3a2e6c53f7 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # 1.1.9-1 works for android, but for ios it becomes 1.1.91, need to set it to 1.1.9-a.1 for iOS, will get 1.1.9.1, but iOS store not allow 4 numbers -version: 1.3.1+48 +version: 1.3.2+51 environment: sdk: '^3.1.0' diff --git a/libs/portable/Cargo.toml b/libs/portable/Cargo.toml index 10d16605b2b3..7e60f7d1fac6 100644 --- a/libs/portable/Cargo.toml +++ b/libs/portable/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rustdesk-portable-packer" -version = "1.3.1" +version = "1.3.2" edition = "2021" description = "RustDesk Remote Desktop" diff --git a/res/PKGBUILD b/res/PKGBUILD index a805997cd888..616682e8f69f 100644 --- a/res/PKGBUILD +++ b/res/PKGBUILD @@ -1,5 +1,5 @@ pkgname=rustdesk -pkgver=1.3.1 +pkgver=1.3.2 pkgrel=0 epoch= pkgdesc="" diff --git a/res/rpm-flutter-suse.spec b/res/rpm-flutter-suse.spec index c2c5be1f188c..768b04c28d2e 100644 --- a/res/rpm-flutter-suse.spec +++ b/res/rpm-flutter-suse.spec @@ -1,5 +1,5 @@ Name: rustdesk -Version: 1.3.1 +Version: 1.3.2 Release: 0 Summary: RPM package License: GPL-3.0 diff --git a/res/rpm-flutter.spec b/res/rpm-flutter.spec index 33a1314ccca6..b62c18b3b71f 100644 --- a/res/rpm-flutter.spec +++ b/res/rpm-flutter.spec @@ -1,5 +1,5 @@ Name: rustdesk -Version: 1.3.1 +Version: 1.3.2 Release: 0 Summary: RPM package License: GPL-3.0 diff --git a/res/rpm.spec b/res/rpm.spec index 11dccc84209c..033e95937d25 100644 --- a/res/rpm.spec +++ b/res/rpm.spec @@ -1,5 +1,5 @@ Name: rustdesk -Version: 1.3.1 +Version: 1.3.2 Release: 0 Summary: RPM package License: GPL-3.0 From beb1084e877f0ae302945f912a3ccf5aaf59ea84 Mon Sep 17 00:00:00 2001 From: hashiguchi <118793426+hashiguchi736@users.noreply.github.com> Date: Fri, 27 Sep 2024 11:14:46 +0900 Subject: [PATCH 236/541] Change the minimum value of the bitrate slider to 5 (#9480) Signed-off-by: hashiguchi --- src/ui/header.tis | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui/header.tis b/src/ui/header.tis index e99d398aa73d..36aa624b48f4 100644 --- a/src/ui/header.tis +++ b/src/ui/header.tis @@ -446,7 +446,7 @@ function handle_custom_image_quality() { var extendedBitrate = bitrate > 100; var maxRate = extendedBitrate ? 2000 : 100; msgbox("custom-image-quality", "Custom Image Quality", "
    \ -
    x% Bitrate More
    \ +
    x% Bitrate More
    \
    ", "", function(res=null) { if (!res) return; if (res.id === "extended-slider") { From 4459406578b9d46bc64eef33a9af32fde34c64c8 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Fri, 27 Sep 2024 14:19:25 +0800 Subject: [PATCH 237/541] fix: windows, window, restore from minimized state (#9482) Signed-off-by: fufesou --- flutter/pubspec.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index 62f9283a0002..b25d395dca66 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -335,7 +335,7 @@ packages: description: path: "." ref: HEAD - resolved-ref: "80b063b9d4e015f62e17f42a5aa0b3d20a365926" + resolved-ref: af2e9a51f18effd9796bebb2fa75e0b7ef079613 url: "https://github.com/rustdesk-org/rustdesk_desktop_multi_window" source: git version: "0.1.0" From 3e6938bec616e2d42f71416a8f4a126b87859909 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Fri, 27 Sep 2024 21:56:10 +0800 Subject: [PATCH 238/541] refact: web desktop, web_id_input_tip (#9490) * refact: web desktop, web_id_input_tip Signed-off-by: fufesou * Update en.rs * Update cn.rs * Update en.rs --------- Signed-off-by: fufesou Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com> --- .../common/widgets/connection_page_title.dart | 38 +++++++++++++++++++ .../lib/desktop/pages/connection_page.dart | 33 +--------------- flutter/lib/mobile/pages/connection_page.dart | 8 +++- src/lang/ar.rs | 1 + src/lang/be.rs | 1 + src/lang/bg.rs | 1 + src/lang/ca.rs | 1 + src/lang/cn.rs | 1 + src/lang/cs.rs | 1 + src/lang/da.rs | 1 + src/lang/de.rs | 1 + src/lang/el.rs | 1 + src/lang/en.rs | 1 + src/lang/eo.rs | 1 + src/lang/es.rs | 1 + src/lang/et.rs | 1 + src/lang/eu.rs | 1 + src/lang/fa.rs | 1 + src/lang/fr.rs | 1 + src/lang/he.rs | 1 + src/lang/hr.rs | 1 + src/lang/hu.rs | 1 + src/lang/id.rs | 1 + src/lang/it.rs | 1 + src/lang/ja.rs | 1 + src/lang/ko.rs | 1 + src/lang/kz.rs | 1 + src/lang/lt.rs | 1 + src/lang/lv.rs | 1 + src/lang/nb.rs | 1 + src/lang/nl.rs | 1 + src/lang/pl.rs | 1 + src/lang/pt_PT.rs | 1 + src/lang/ptbr.rs | 1 + src/lang/ro.rs | 1 + src/lang/ru.rs | 1 + src/lang/sk.rs | 1 + src/lang/sl.rs | 1 + src/lang/sq.rs | 1 + src/lang/sr.rs | 1 + src/lang/sv.rs | 1 + src/lang/template.rs | 1 + src/lang/th.rs | 1 + src/lang/tr.rs | 1 + src/lang/tw.rs | 1 + src/lang/uk.rs | 1 + src/lang/vn.rs | 1 + 47 files changed, 91 insertions(+), 32 deletions(-) create mode 100644 flutter/lib/common/widgets/connection_page_title.dart diff --git a/flutter/lib/common/widgets/connection_page_title.dart b/flutter/lib/common/widgets/connection_page_title.dart new file mode 100644 index 000000000000..ba03c2656960 --- /dev/null +++ b/flutter/lib/common/widgets/connection_page_title.dart @@ -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), + ), + ), + ], + )), + ], + ); +} diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index b2073ae4a573..744c05f9c223 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -3,8 +3,8 @@ import 'dart:async'; import 'dart:convert'; -import 'package:auto_size_text/auto_size_text.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/models/state_model.dart'; import 'package:get/get.dart'; @@ -323,36 +323,7 @@ class _ConnectionPageState extends State child: Ink( child: Column( children: [ - 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("id_input_tip"), - child: Icon( - Icons.help_outline_outlined, - size: 16, - color: Theme.of(context) - .textTheme - .titleLarge - ?.color - ?.withOpacity(0.5), - ), - ), - ], - )), - ], - ).marginOnly(bottom: 15), + getConnectionPageTitle(context, false).marginOnly(bottom: 15), Row( children: [ Expanded( diff --git a/flutter/lib/mobile/pages/connection_page.dart b/flutter/lib/mobile/pages/connection_page.dart index 181f36e58cc6..e71cc1c56470 100644 --- a/flutter/lib/mobile/pages/connection_page.dart +++ b/flutter/lib/mobile/pages/connection_page.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:auto_size_text_field/auto_size_text_field.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/common/formatter/id_formatter.dart'; +import 'package:flutter_hbb/common/widgets/connection_page_title.dart'; import 'package:get/get.dart'; import 'package:provider/provider.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -349,9 +350,14 @@ class _ConnectionPageState extends State { ), ), ); + final child = Column(children: [ + if (isWebDesktop) + getConnectionPageTitle(context, true).marginOnly(bottom: 10, top: 15, left: 12), + w + ]); return Align( alignment: Alignment.topCenter, - child: Container(constraints: kMobilePageConstraints, child: w)); + child: Container(constraints: kMobilePageConstraints, child: child)); } @override diff --git a/src/lang/ar.rs b/src/lang/ar.rs index be1a6b7675aa..0990614e2f0c 100644 --- a/src/lang/ar.rs +++ b/src/lang/ar.rs @@ -647,5 +647,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("one-way-file-transfer-tip", ""), ("Authentication Required", ""), ("Authenticate", ""), + ("web_id_input_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/be.rs b/src/lang/be.rs index fb8444becb5e..2b9167a9cc26 100644 --- a/src/lang/be.rs +++ b/src/lang/be.rs @@ -647,5 +647,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("one-way-file-transfer-tip", ""), ("Authentication Required", ""), ("Authenticate", ""), + ("web_id_input_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/bg.rs b/src/lang/bg.rs index b683a5293c45..7e7e7a05c213 100644 --- a/src/lang/bg.rs +++ b/src/lang/bg.rs @@ -647,5 +647,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("one-way-file-transfer-tip", ""), ("Authentication Required", ""), ("Authenticate", ""), + ("web_id_input_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ca.rs b/src/lang/ca.rs index a52da9735fdb..ef53608de827 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -647,5 +647,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("one-way-file-transfer-tip", ""), ("Authentication Required", ""), ("Authenticate", ""), + ("web_id_input_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 99b3284cbc6b..cca8b75031d7 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -647,5 +647,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("one-way-file-transfer-tip", "被控端启用了单向文件传输"), ("Authentication Required", "需要身份验证"), ("Authenticate", "认证"), + ("web_id_input_tip", "可以输入同一个服务器内的 ID, web 客户端不支持直接 IP 访问。\n要访问另一台服务器上的设备,请附加服务器地址(@<服务器地址>?key=<密钥>)。比如,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=。\n要访问公共服务器上的设备,请输入 \"@public\", 无需密钥。"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index 67588bfb8486..c93dd37b7f91 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -647,5 +647,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("one-way-file-transfer-tip", ""), ("Authentication Required", ""), ("Authenticate", ""), + ("web_id_input_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index aea1514ae936..f59accfe1c79 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -647,5 +647,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("one-way-file-transfer-tip", ""), ("Authentication Required", ""), ("Authenticate", ""), + ("web_id_input_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index d4f72c94b016..826f97b88d58 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -647,5 +647,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("one-way-file-transfer-tip", "Die einseitige Dateiübertragung ist auf der kontrollierten Seite aktiviert."), ("Authentication Required", "Authentifizierung erforderlich"), ("Authenticate", "Authentifizieren"), + ("web_id_input_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/el.rs b/src/lang/el.rs index fecff28945fe..779e71d23ea2 100644 --- a/src/lang/el.rs +++ b/src/lang/el.rs @@ -647,5 +647,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("one-way-file-transfer-tip", ""), ("Authentication Required", ""), ("Authenticate", ""), + ("web_id_input_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/en.rs b/src/lang/en.rs index 55ef4470c55b..7ed83a8fe4f0 100644 --- a/src/lang/en.rs +++ b/src/lang/en.rs @@ -235,5 +235,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("network_error_tip", "Please check your network connection, then click retry."), ("enable-trusted-devices-tip", "Skip 2FA verification on trusted devices"), ("one-way-file-transfer-tip", "One-way file transfer is enabled on the controlled side."), + ("web_id_input_tip", "You can input an ID in the same server, direct IP access is not supported in web client.\nIf you want to access a device on another server, please append the server address (@?key=), for example,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nIf you want to access a device on a public server, please input \"@public\", the key is not needed for public server."), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index eb512922ed3c..3a406e558710 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -647,5 +647,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("one-way-file-transfer-tip", ""), ("Authentication Required", ""), ("Authenticate", ""), + ("web_id_input_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index cb5e4a25b397..369cd5b95158 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -647,5 +647,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("one-way-file-transfer-tip", "La transferencia en un sentido está habilitada en el lado controlado."), ("Authentication Required", "Se requiere autenticación"), ("Authenticate", "Autenticar"), + ("web_id_input_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/et.rs b/src/lang/et.rs index 2443faae9b01..db7970f16e45 100644 --- a/src/lang/et.rs +++ b/src/lang/et.rs @@ -647,5 +647,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("one-way-file-transfer-tip", ""), ("Authentication Required", ""), ("Authenticate", ""), + ("web_id_input_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eu.rs b/src/lang/eu.rs index 7c953ebe46e1..90976236e574 100644 --- a/src/lang/eu.rs +++ b/src/lang/eu.rs @@ -647,5 +647,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("one-way-file-transfer-tip", ""), ("Authentication Required", ""), ("Authenticate", ""), + ("web_id_input_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index a97566d99a13..71bea4e78929 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -647,5 +647,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("one-way-file-transfer-tip", ""), ("Authentication Required", ""), ("Authenticate", ""), + ("web_id_input_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 6a9ca8d8de20..97f8d651406f 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -647,5 +647,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("one-way-file-transfer-tip", ""), ("Authentication Required", ""), ("Authenticate", ""), + ("web_id_input_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/he.rs b/src/lang/he.rs index 5a42c4257c70..526873de3d2d 100644 --- a/src/lang/he.rs +++ b/src/lang/he.rs @@ -647,5 +647,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("one-way-file-transfer-tip", ""), ("Authentication Required", ""), ("Authenticate", ""), + ("web_id_input_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hr.rs b/src/lang/hr.rs index 9a2b8e3a1a02..e210e7566b6d 100644 --- a/src/lang/hr.rs +++ b/src/lang/hr.rs @@ -647,5 +647,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("one-way-file-transfer-tip", ""), ("Authentication Required", ""), ("Authenticate", ""), + ("web_id_input_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index fb748b6b80dd..ccb2be1cb02e 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -647,5 +647,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("one-way-file-transfer-tip", ""), ("Authentication Required", ""), ("Authenticate", ""), + ("web_id_input_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index 9ed642459c15..5d5d06a662fb 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -647,5 +647,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("one-way-file-transfer-tip", ""), ("Authentication Required", ""), ("Authenticate", ""), + ("web_id_input_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index a07df2da9227..7f49e60452ec 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -647,5 +647,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("one-way-file-transfer-tip", "Sul lato controllato è abilitato il trasferimento file unidirezionale."), ("Authentication Required", "Richiesta autenticazione"), ("Authenticate", "Autentica"), + ("web_id_input_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index a63687c68f7b..eca28da863ea 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -647,5 +647,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("one-way-file-transfer-tip", ""), ("Authentication Required", ""), ("Authenticate", ""), + ("web_id_input_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 9d9c29f9af2a..a3c24e98cd75 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -647,5 +647,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("one-way-file-transfer-tip", "단방향 파일 전송은 제어되는 쪽에서 활성화됩니다."), ("Authentication Required", "인증 필요함"), ("Authenticate", "인증"), + ("web_id_input_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index a23d34448304..404e9b511af8 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -647,5 +647,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("one-way-file-transfer-tip", ""), ("Authentication Required", ""), ("Authenticate", ""), + ("web_id_input_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lt.rs b/src/lang/lt.rs index b818cb7e76ea..a93cef88c3db 100644 --- a/src/lang/lt.rs +++ b/src/lang/lt.rs @@ -647,5 +647,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("one-way-file-transfer-tip", ""), ("Authentication Required", ""), ("Authenticate", ""), + ("web_id_input_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lv.rs b/src/lang/lv.rs index 63a4a7dae4c8..0cddcb951b26 100644 --- a/src/lang/lv.rs +++ b/src/lang/lv.rs @@ -647,5 +647,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("one-way-file-transfer-tip", "Kontrolējamajā pusē ir iespējota vienvirziena failu pārsūtīšana."), ("Authentication Required", "Nepieciešama autentifikācija"), ("Authenticate", "Autentificēt"), + ("web_id_input_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nb.rs b/src/lang/nb.rs index eb2e21b683c1..7f08f83d10c4 100644 --- a/src/lang/nb.rs +++ b/src/lang/nb.rs @@ -647,5 +647,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("one-way-file-transfer-tip", ""), ("Authentication Required", ""), ("Authenticate", ""), + ("web_id_input_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nl.rs b/src/lang/nl.rs index 6a0fa3e702a1..bd1dc8d83f37 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -647,5 +647,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("one-way-file-transfer-tip", "Eenzijdige bestandsoverdracht is ingeschakeld aan de gecontroleerde kant."), ("Authentication Required", "Verificatie vereist"), ("Authenticate", "Verificatie"), + ("web_id_input_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 99bf570848f0..a7216e780415 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -647,5 +647,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("one-way-file-transfer-tip", ""), ("Authentication Required", ""), ("Authenticate", ""), + ("web_id_input_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index 534efe12e68b..994d5cc63db1 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -647,5 +647,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("one-way-file-transfer-tip", ""), ("Authentication Required", ""), ("Authenticate", ""), + ("web_id_input_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index 4396dd40de38..ba6fe051b81b 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -647,5 +647,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("one-way-file-transfer-tip", ""), ("Authentication Required", ""), ("Authenticate", ""), + ("web_id_input_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ro.rs b/src/lang/ro.rs index ea1d98880147..429c2534ab7b 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -647,5 +647,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("one-way-file-transfer-tip", ""), ("Authentication Required", ""), ("Authenticate", ""), + ("web_id_input_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index ba8d33f95db2..8ad598024868 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -647,5 +647,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("one-way-file-transfer-tip", "На управляемой стороне включена односторонняя передача файлов."), ("Authentication Required", "Требуется аутентификация"), ("Authenticate", "Аутентификация"), + ("web_id_input_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index e7b17c6aea80..1e5a51f9f551 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -647,5 +647,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("one-way-file-transfer-tip", ""), ("Authentication Required", ""), ("Authenticate", ""), + ("web_id_input_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sl.rs b/src/lang/sl.rs index acbaf2ea9dba..bba9499d75ad 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -647,5 +647,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("one-way-file-transfer-tip", ""), ("Authentication Required", ""), ("Authenticate", ""), + ("web_id_input_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sq.rs b/src/lang/sq.rs index aca5b6a7aceb..b3e85a351e1d 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -647,5 +647,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("one-way-file-transfer-tip", ""), ("Authentication Required", ""), ("Authenticate", ""), + ("web_id_input_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sr.rs b/src/lang/sr.rs index c12555c97d22..ee84a68e1b81 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -647,5 +647,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("one-way-file-transfer-tip", ""), ("Authentication Required", ""), ("Authenticate", ""), + ("web_id_input_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sv.rs b/src/lang/sv.rs index 837f702f8896..9ba9bb5362ad 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -647,5 +647,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("one-way-file-transfer-tip", ""), ("Authentication Required", ""), ("Authenticate", ""), + ("web_id_input_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index 43fbbcfa5097..e62119843480 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -647,5 +647,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("one-way-file-transfer-tip", ""), ("Authentication Required", ""), ("Authenticate", ""), + ("web_id_input_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/th.rs b/src/lang/th.rs index 1c5b09333774..a2f8bc9103ce 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -647,5 +647,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("one-way-file-transfer-tip", ""), ("Authentication Required", ""), ("Authenticate", ""), + ("web_id_input_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index 10fa46880f44..78b09dca47e9 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -647,5 +647,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("one-way-file-transfer-tip", ""), ("Authentication Required", ""), ("Authenticate", ""), + ("web_id_input_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 0a3f291ac974..7708de9ecd42 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -647,5 +647,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("one-way-file-transfer-tip", "被控端啟用了單向文件傳輸"), ("Authentication Required", "需要身分驗證"), ("Authenticate", "認證"), + ("web_id_input_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/uk.rs b/src/lang/uk.rs index 41634facc398..05031dcc9306 100644 --- a/src/lang/uk.rs +++ b/src/lang/uk.rs @@ -647,5 +647,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("one-way-file-transfer-tip", "На керованій стороні ввімкнено одностороннє передавання файлів."), ("Authentication Required", "Потрібна автентифікація"), ("Authenticate", "Автентифікувати"), + ("web_id_input_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index 4f7bcf7d1646..bc6311fcd9ce 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -647,5 +647,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("one-way-file-transfer-tip", ""), ("Authentication Required", ""), ("Authenticate", ""), + ("web_id_input_tip", ""), ].iter().cloned().collect(); } From 9959217cc35b05248c252a0dba5301689dd24ce2 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Fri, 27 Sep 2024 23:10:51 +0800 Subject: [PATCH 239/541] chore (#9491) Signed-off-by: fufesou --- src/lang/cn.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lang/cn.rs b/src/lang/cn.rs index cca8b75031d7..db942f598f05 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -563,7 +563,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Plug out all", "拔出所有"), ("True color (4:4:4)", "真彩模式(4:4:4)"), ("Enable blocking user input", "允许阻止用户输入"), - ("id_input_tip", "可以输入 ID、直连 IP,或域名和端口号(<域名>:<端口号>)。\n要访问另一台服务器上的设备,请附加服务器地址(@<服务器地址>?key=<密钥>)。比如,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=。\n要访问公共服务器上的设备,请输入 \"@public\", 无需密钥。\n\n如果您想要在首次连接时,强制走中继连接,请在 ID 的后面添加 \"/r\",例如,\"9123456234/r\"。"), + ("id_input_tip", "可以输入 ID、直连 IP,或域名和端口号(<域名>:<端口号>)。\n要访问另一台服务器上的设备,请附加服务器地址(@<服务器地址>?key=<密钥>)。比如,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=。\n要访问公共服务器上的设备,请输入 \"@public\",无需密钥。\n\n如果您想要在首次连接时,强制走中继连接,请在 ID 的后面添加 \"/r\",例如,\"9123456234/r\"。"), ("privacy_mode_impl_mag_tip", "模式 1"), ("privacy_mode_impl_virtual_display_tip", "模式 2"), ("Enter privacy mode", "进入隐私模式"), @@ -647,6 +647,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("one-way-file-transfer-tip", "被控端启用了单向文件传输"), ("Authentication Required", "需要身份验证"), ("Authenticate", "认证"), - ("web_id_input_tip", "可以输入同一个服务器内的 ID, web 客户端不支持直接 IP 访问。\n要访问另一台服务器上的设备,请附加服务器地址(@<服务器地址>?key=<密钥>)。比如,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=。\n要访问公共服务器上的设备,请输入 \"@public\", 无需密钥。"), + ("web_id_input_tip", "可以输入同一个服务器内的 ID,web 客户端不支持直接 IP 访问。\n要访问另一台服务器上的设备,请附加服务器地址(@<服务器地址>?key=<密钥>)。比如,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=。\n要访问公共服务器上的设备,请输入 \"@public\",无需密钥。"), ].iter().cloned().collect(); } From 81b999cfbea26bcc23391c93965d6293a267c87b Mon Sep 17 00:00:00 2001 From: Mr-Update <37781396+Mr-Update@users.noreply.github.com> Date: Sat, 28 Sep 2024 03:25:35 +0200 Subject: [PATCH 240/541] Update de.rs (#9496) --- src/lang/de.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lang/de.rs b/src/lang/de.rs index 826f97b88d58..4a7bb08dbf76 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -563,7 +563,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Plug out all", "Alle ausschalten"), ("True color (4:4:4)", "True Color (4:4:4)"), ("Enable blocking user input", "Blockieren von Benutzereingaben aktivieren"), - ("id_input_tip", "Sie können eine ID, eine direkte IP oder eine Domäne mit einem Port (:) eingeben.\nWenn Sie auf ein Gerät auf einem anderen Server zugreifen möchten, fügen Sie bitte die Serveradresse (@?key=) hinzu, zum Beispiel\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nWenn Sie auf ein Gerät auf einem öffentlichen Server zugreifen wollen, geben Sie bitte \"@public\" ein. Der Schlüssel wird für öffentliche Server nicht benötigt.\n\nWenn Sie bei der ersten Verbindung die Verwendung einer Relay-Verbindung erzwingen wollen, fügen Sie \"/r\" am Ende der ID hinzu, zum Beispiel \"9123456234/r\"."), + ("id_input_tip", "Sie können eine ID, eine direkte IP oder eine Domäne mit einem Port (:) eingeben.\nWenn Sie auf ein Gerät auf einem anderen Server zugreifen wollen, fügen Sie bitte die Serveradresse (@?key=) hinzu, zum Beispiel\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nWenn Sie auf ein Gerät auf einem öffentlichen Server zugreifen wollen, geben Sie bitte \"@public\" ein. Der Schlüssel wird für öffentliche Server nicht benötigt.\n\nWenn Sie bei der ersten Verbindung die Verwendung einer Relay-Verbindung erzwingen wollen, fügen Sie \"/r\" am Ende der ID hinzu, zum Beispiel \"9123456234/r\"."), ("privacy_mode_impl_mag_tip", "Modus 1"), ("privacy_mode_impl_virtual_display_tip", "Modus 2"), ("Enter privacy mode", "Datenschutzmodus aktivieren"), @@ -647,6 +647,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("one-way-file-transfer-tip", "Die einseitige Dateiübertragung ist auf der kontrollierten Seite aktiviert."), ("Authentication Required", "Authentifizierung erforderlich"), ("Authenticate", "Authentifizieren"), - ("web_id_input_tip", ""), + ("web_id_input_tip", "Sie können eine ID auf demselben Server eingeben, direkter IP-Zugriff wird im Web-Client nicht unterstützt.\nWenn Sie auf ein Gerät auf einem anderen Server zugreifen wollen, fügen Sie bitte die Serveradresse (@?key=) hinzu, zum Beispiel\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nWenn Sie auf ein Gerät auf einem öffentlichen Server zugreifen wollen, geben Sie bitte \"@public\" ein. Der Schlüssel wird für öffentliche Server nicht benötigt."), ].iter().cloned().collect(); } From 769bbf1e1c82f0bb5f5065ebbca6d5094e345f05 Mon Sep 17 00:00:00 2001 From: solokot Date: Sat, 28 Sep 2024 04:25:52 +0300 Subject: [PATCH 241/541] Update ru.rs (#9495) --- src/lang/ru.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 8ad598024868..3ab54b42a083 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -647,6 +647,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("one-way-file-transfer-tip", "На управляемой стороне включена односторонняя передача файлов."), ("Authentication Required", "Требуется аутентификация"), ("Authenticate", "Аутентификация"), - ("web_id_input_tip", ""), + ("web_id_input_tip", "Можно ввести ID на том же сервере, прямой доступ по IP в веб-клиенте не поддерживается.\nЕсли вы хотите получить доступ к устройству на другом сервере, добавьте адрес сервера (@<адрес_сервера>?key=<ключ>), например,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nЕсли вы хотите получить доступ к устройству на публичном сервере, введите \"@public\", для публичного сервера ключ не нужен."), ].iter().cloned().collect(); } From 3365844defe413e58e626ae59aedfe7e3050a308 Mon Sep 17 00:00:00 2001 From: bovirus <1262554+bovirus@users.noreply.github.com> Date: Sat, 28 Sep 2024 03:26:05 +0200 Subject: [PATCH 242/541] Update Italian language (#9493) --- src/lang/it.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lang/it.rs b/src/lang/it.rs index 7f49e60452ec..4d9ead853c8d 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -369,7 +369,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop session recording", "Ferma registrazione sessione"), ("Enable recording session", "Abilita registrazione sessione"), ("Enable LAN discovery", "Abilita rilevamento LAN"), - ("Deny LAN discovery", "Disabilita rilevamento LAN"), + ("Deny LAN discovery", "Non effettuare rilevamento LAN"), ("Write a message", "Scrivi un messaggio"), ("Prompt", "Richiedi"), ("Please wait for confirmation of UAC...", "Attendi la conferma dell'UAC..."), @@ -647,6 +647,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("one-way-file-transfer-tip", "Sul lato controllato è abilitato il trasferimento file unidirezionale."), ("Authentication Required", "Richiesta autenticazione"), ("Authenticate", "Autentica"), - ("web_id_input_tip", ""), + ("web_id_input_tip", "È possibile inserire un ID nello stesso server, nel client web non è supportato l'accesso con IP diretto.\nSe vuoi accedere ad un dispositivo in un altro server, aggiungi l'indirizzo del server (@?key=), ad esempio,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nSe vuoi accedere ad un dispositivo in un server pubblico, inserisci \"@public\", la chiave non è necessaria per il server pubblico."), ].iter().cloned().collect(); } From f6261883e81a6012c60d61f7c447af49540529f8 Mon Sep 17 00:00:00 2001 From: Alex Rijckaert Date: Sat, 28 Sep 2024 09:33:08 +0200 Subject: [PATCH 243/541] Update nl.rs (#9500) --- src/lang/nl.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lang/nl.rs b/src/lang/nl.rs index bd1dc8d83f37..dc94f4235aa1 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -647,6 +647,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("one-way-file-transfer-tip", "Eenzijdige bestandsoverdracht is ingeschakeld aan de gecontroleerde kant."), ("Authentication Required", "Verificatie vereist"), ("Authenticate", "Verificatie"), - ("web_id_input_tip", ""), + ("web_id_input_tip", "Het is mogelijk om een ID in te voeren in de server zelf, in de webclient wordt directe IP-toegang niet ondersteund.\nAls je toegang wilt krijgen tot een apparaat op een andere server, voeg dan bijvoorbeeld het serveradres toe (@?key=),\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nAls je toegang wilt krijgen tot een apparaat op een publieke server, voer dan \“@public\” in, de sleutel is niet nodig voor de publieke server. ].iter().cloned().collect(); } From 1e822fa13577f71538e29ae1ef50686c597c1cf0 Mon Sep 17 00:00:00 2001 From: RustDesk <71636191+rustdesk@users.noreply.github.com> Date: Sat, 28 Sep 2024 15:49:11 +0800 Subject: [PATCH 244/541] Update nl.rs (#9501) --- src/lang/nl.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lang/nl.rs b/src/lang/nl.rs index dc94f4235aa1..960aa6234aed 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -647,6 +647,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("one-way-file-transfer-tip", "Eenzijdige bestandsoverdracht is ingeschakeld aan de gecontroleerde kant."), ("Authentication Required", "Verificatie vereist"), ("Authenticate", "Verificatie"), - ("web_id_input_tip", "Het is mogelijk om een ID in te voeren in de server zelf, in de webclient wordt directe IP-toegang niet ondersteund.\nAls je toegang wilt krijgen tot een apparaat op een andere server, voeg dan bijvoorbeeld het serveradres toe (@?key=),\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nAls je toegang wilt krijgen tot een apparaat op een publieke server, voer dan \“@public\” in, de sleutel is niet nodig voor de publieke server. + ("web_id_input_tip", "Het is mogelijk om een ID in te voeren in de server zelf, in de webclient wordt directe IP-toegang niet ondersteund.\nAls je toegang wilt krijgen tot een apparaat op een andere server, voeg dan bijvoorbeeld het serveradres toe (@?key=),\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nAls je toegang wilt krijgen tot een apparaat op een publieke server, voer dan \“@public\” in, de sleutel is niet nodig voor de publieke server.“), ].iter().cloned().collect(); } From 30a7847100857db44ec31614f68c5b8bc204047e Mon Sep 17 00:00:00 2001 From: rustdesk Date: Sat, 28 Sep 2024 15:53:39 +0800 Subject: [PATCH 245/541] fix ci --- src/lang/nl.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lang/nl.rs b/src/lang/nl.rs index 960aa6234aed..df1d6a1e7e4f 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -647,6 +647,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("one-way-file-transfer-tip", "Eenzijdige bestandsoverdracht is ingeschakeld aan de gecontroleerde kant."), ("Authentication Required", "Verificatie vereist"), ("Authenticate", "Verificatie"), - ("web_id_input_tip", "Het is mogelijk om een ID in te voeren in de server zelf, in de webclient wordt directe IP-toegang niet ondersteund.\nAls je toegang wilt krijgen tot een apparaat op een andere server, voeg dan bijvoorbeeld het serveradres toe (@?key=),\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nAls je toegang wilt krijgen tot een apparaat op een publieke server, voer dan \“@public\” in, de sleutel is niet nodig voor de publieke server.“), + ("web_id_input_tip", "Het is mogelijk om een ID in te voeren in de server zelf, in de webclient wordt directe IP-toegang niet ondersteund.\nAls je toegang wilt krijgen tot een apparaat op een andere server, voeg dan bijvoorbeeld het serveradres toe (@?key=),\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nAls je toegang wilt krijgen tot een apparaat op een publieke server, voer dan \"@public\" in, de sleutel is niet nodig voor de publieke server."), ].iter().cloned().collect(); } From 60a0099ba0a6ecf7b3e2a920964912e66228853d Mon Sep 17 00:00:00 2001 From: Iraccib <56926862+Iraccib@users.noreply.github.com> Date: Sat, 28 Sep 2024 12:04:31 +0200 Subject: [PATCH 246/541] Update it.rs (#9503) Changed "Scehrmo" to "Schermo" --- src/lang/it.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lang/it.rs b/src/lang/it.rs index 4d9ead853c8d..68a24265999e 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -559,7 +559,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Big tiles", "Icone grandi"), ("Small tiles", "Icone piccole"), ("List", "Elenco"), - ("Virtual display", "Scehrmo virtuale"), + ("Virtual display", "Schermo virtuale"), ("Plug out all", "Scollega tutto"), ("True color (4:4:4)", "Colore reale (4:4:4)"), ("Enable blocking user input", "Abilita blocco input utente"), From 2f5f701dc70b3638075e0731e203de90f2e84aa2 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Sat, 28 Sep 2024 19:43:28 +0800 Subject: [PATCH 247/541] fix: windows, subwindow, scale (#9506) Signed-off-by: fufesou --- flutter/pubspec.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index b25d395dca66..5a4a74ed6769 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -335,7 +335,7 @@ packages: description: path: "." ref: HEAD - resolved-ref: af2e9a51f18effd9796bebb2fa75e0b7ef079613 + resolved-ref: ad889c35b896435eb7c828e200db2fb6f41d2c3b url: "https://github.com/rustdesk-org/rustdesk_desktop_multi_window" source: git version: "0.1.0" From 4a745d82f63abbd0718c68cce4fabf8cd6c892be Mon Sep 17 00:00:00 2001 From: Alex Rijckaert Date: Sat, 28 Sep 2024 14:07:55 +0200 Subject: [PATCH 248/541] Update nl.rs (#9507) --- src/lang/nl.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lang/nl.rs b/src/lang/nl.rs index df1d6a1e7e4f..1b8a96e540d3 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -647,6 +647,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("one-way-file-transfer-tip", "Eenzijdige bestandsoverdracht is ingeschakeld aan de gecontroleerde kant."), ("Authentication Required", "Verificatie vereist"), ("Authenticate", "Verificatie"), - ("web_id_input_tip", "Het is mogelijk om een ID in te voeren in de server zelf, in de webclient wordt directe IP-toegang niet ondersteund.\nAls je toegang wilt krijgen tot een apparaat op een andere server, voeg dan bijvoorbeeld het serveradres toe (@?key=),\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nAls je toegang wilt krijgen tot een apparaat op een publieke server, voer dan \"@public\" in, de sleutel is niet nodig voor de publieke server."), + ("web_id_input_tip", "Je kunt een ID invoeren op dezelfde server, directe IP-toegang wordt niet ondersteund in de webclient.\nAls je toegang wilt tot een apparaat op een andere server, voeg je het serveradres toe (@?key=), bijvoorbeeld,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nAls je toegang wilt krijgen tot een apparaat op een publieke server, voer dan \"@public\" in, sleutel is niet nodig voor de publieke server."), ].iter().cloned().collect(); } From d563372a91a1b1dcb552f6571828ccf081ea3f42 Mon Sep 17 00:00:00 2001 From: hashiguchi <118793426+hashiguchi736@users.noreply.github.com> Date: Sat, 28 Sep 2024 22:52:05 +0900 Subject: [PATCH 249/541] Change the value of kMinQuality to 5 (#9508) * Change the minimum value of the bitrate slider to 5 Signed-off-by: hashiguchi * Change the value of kMinQuality to 5 Signed-off-by: hashiguchi --------- Signed-off-by: hashiguchi --- flutter/lib/consts.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index 17d5cec271b8..1be9c7712c79 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -201,7 +201,7 @@ const double kMinFps = 5; const double kDefaultFps = 30; const double kMaxFps = 120; -const double kMinQuality = 10; +const double kMinQuality = 5; const double kDefaultQuality = 50; const double kMaxQuality = 100; const double kMaxMoreQuality = 2000; @@ -571,4 +571,4 @@ extension WindowsTargetExt on int { WindowsTarget get windowsVersion => getWindowsTarget(this); } -const kCheckSoftwareUpdateFinish = 'check_software_update_finish'; \ No newline at end of file +const kCheckSoftwareUpdateFinish = 'check_software_update_finish'; From 1ebc726acdd46d902d3c6444be8ede3152e65598 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Mon, 30 Sep 2024 10:54:05 +0800 Subject: [PATCH 250/541] fix ci --- flatpak/rustdesk.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flatpak/rustdesk.json b/flatpak/rustdesk.json index 6b205aa99925..6d7acb5b89ca 100644 --- a/flatpak/rustdesk.json +++ b/flatpak/rustdesk.json @@ -17,7 +17,7 @@ "sources": [ { "type": "archive", - "url": "https://github.com/linux-pam/linux-pam/releases/download/v1.3.2/Linux-PAM-1.3.2.tar.xz", + "url": "https://github.com/linux-pam/linux-pam/releases/download/v1.3.1/Linux-PAM-1.3.1.tar.xz", "sha256": "eff47a4ecd833fbf18de9686632a70ee8d0794b79aecb217ebd0ce11db4cd0db" } ] From 4eca8b944797e32a9bc7755ca7ad0635bdec995a Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Tue, 1 Oct 2024 07:04:43 +0800 Subject: [PATCH 251/541] fix: web, forget password (#9524) Signed-off-by: fufesou --- flutter/lib/web/bridge.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/lib/web/bridge.dart b/flutter/lib/web/bridge.dart index 6e758dae438c..5f699114ae05 100644 --- a/flutter/lib/web/bridge.dart +++ b/flutter/lib/web/bridge.dart @@ -936,7 +936,7 @@ class RustdeskImpl { } Future mainForgetPassword({required String id, dynamic hint}) { - return Future(() => js.context.callMethod('setByName', ['forget'])); + return mainSetPeerOption(id: id, key: 'password', value: ''); } Future mainPeerHasPassword({required String id, dynamic hint}) { From b5414ec002b605dda20d7a57772a426c3ab29bc5 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Tue, 1 Oct 2024 07:09:17 +0800 Subject: [PATCH 252/541] fix https://github.com/rustdesk/rustdesk/issues/9527 --- flutter/lib/desktop/pages/file_manager_page.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index 4677744197ed..b75a946c06be 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -1020,7 +1020,7 @@ class _FileManagerViewState extends State { if (!entry.isDrive && versionCmp(_ffi.ffiModel.pi.version, "1.3.0") >= 0) mod_menu.PopupMenuItem( - child: Text("Rename"), + child: Text(translate("Rename")), height: CustomPopupMenuTheme.height, onTap: () { controller.renameAction(entry, isLocal); From 334526026cad032376166fa4b69f97db4bc63806 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Tue, 1 Oct 2024 15:25:59 +0800 Subject: [PATCH 253/541] fix: web/mobile, skip querying onlines, if not in main page (#9535) * fix: web, skip querying onlines, if not in main page Signed-off-by: fufesou * fix: web/mobile, skip querying onlines Signed-off-by: fufesou * Set isInMainPage to false after router is changed. Signed-off-by: fufesou --------- Signed-off-by: fufesou --- flutter/lib/common.dart | 3 +++ flutter/lib/common/widgets/peers_view.dart | 7 ++++++- flutter/lib/mobile/pages/home_page.dart | 2 ++ flutter/lib/models/state_model.dart | 3 +++ flutter/lib/models/web_model.dart | 11 ++++++++++- flutter/lib/web/settings_page.dart | 1 - 6 files changed, 24 insertions(+), 3 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 7b6ec4ae5fc8..99a24781b957 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -680,10 +680,12 @@ closeConnection({String? id}) { overlays: SystemUiOverlay.values); gFFI.chatModel.hideChatOverlay(); Navigator.popUntil(globalKey.currentContext!, ModalRoute.withName("/")); + stateGlobal.isInMainPage = true; }(); } else { if (isWeb) { Navigator.popUntil(globalKey.currentContext!, ModalRoute.withName("/")); + stateGlobal.isInMainPage = true; } else { final controller = Get.find(); controller.closeBy(id); @@ -2405,6 +2407,7 @@ connect(BuildContext context, String id, ); } } + stateGlobal.isInMainPage = false; } FocusScopeNode currentFocus = FocusScope.of(context); diff --git a/flutter/lib/common/widgets/peers_view.dart b/flutter/lib/common/widgets/peers_view.dart index 32db418f5bba..7f16850219f4 100644 --- a/flutter/lib/common/widgets/peers_view.dart +++ b/flutter/lib/common/widgets/peers_view.dart @@ -332,7 +332,12 @@ class _PeersViewState extends State<_PeersView> _queryOnlines(false); } } else { - if (_isActive && (_queryCount < _maxQueryCount || !p)) { + 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 (_curPeers.isNotEmpty) { bind.queryOnlines(ids: _curPeers.toList(growable: false)); diff --git a/flutter/lib/mobile/pages/home_page.dart b/flutter/lib/mobile/pages/home_page.dart index d26d91685420..e329acdfe17e 100644 --- a/flutter/lib/mobile/pages/home_page.dart +++ b/flutter/lib/mobile/pages/home_page.dart @@ -6,6 +6,7 @@ import 'package:get/get.dart'; import '../../common.dart'; import '../../common/widgets/chat_page.dart'; import '../../models/platform_model.dart'; +import '../../models/state_model.dart'; import 'connection_page.dart'; abstract class PageShape extends Widget { @@ -159,6 +160,7 @@ class WebHomePage extends StatelessWidget { @override Widget build(BuildContext context) { + stateGlobal.isInMainPage = true; return Scaffold( // backgroundColor: MyTheme.grayBg, appBar: AppBar( diff --git a/flutter/lib/models/state_model.dart b/flutter/lib/models/state_model.dart index e18874785cf6..a1b5fc736e9b 100644 --- a/flutter/lib/models/state_model.dart +++ b/flutter/lib/models/state_model.dart @@ -19,6 +19,9 @@ class StateGlobal { final RxBool showRemoteToolBar = false.obs; final svcStatus = SvcStatus.notReady.obs; final RxBool isFocused = false.obs; + // for mobile and web + bool isInMainPage = true; + bool isWebVisible = true; final isPortrait = false.obs; diff --git a/flutter/lib/models/web_model.dart b/flutter/lib/models/web_model.dart index 4896781a9cc1..ff0d1b806dad 100644 --- a/flutter/lib/models/web_model.dart +++ b/flutter/lib/models/web_model.dart @@ -7,6 +7,7 @@ import 'dart:html'; import 'dart:async'; import 'package:flutter/foundation.dart'; +import 'package:flutter_hbb/models/state_model.dart'; import 'package:flutter_hbb/web/bridge.dart'; import 'package:flutter_hbb/common.dart'; @@ -28,7 +29,15 @@ class PlatformFFI { context.callMethod('setByName', [name, value]); } - PlatformFFI._(); + PlatformFFI._() { + window.document.addEventListener( + 'visibilitychange', + (event) => { + stateGlobal.isWebVisible = + window.document.visibilityState == 'visible' + }); + } + static final PlatformFFI instance = PlatformFFI._(); static get localeName => window.navigator.language; diff --git a/flutter/lib/web/settings_page.dart b/flutter/lib/web/settings_page.dart index 13ba6cb2f275..cbd79a718ecd 100644 --- a/flutter/lib/web/settings_page.dart +++ b/flutter/lib/web/settings_page.dart @@ -95,4 +95,3 @@ class WebSettingsPage extends StatelessWidget { }); } } - From b0edfb8f7027bf8ad7284c47095118a056c0f158 Mon Sep 17 00:00:00 2001 From: hms5232 <43672033+hms5232@users.noreply.github.com> Date: Thu, 3 Oct 2024 15:01:53 +0800 Subject: [PATCH 254/541] fix wrong terms in tw lang (#9541) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "port" is "通訊埠", "連接埠" or just "埠" in Taiwan, "端口" is used in China. --- src/lang/tw.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 7708de9ecd42..c935b5552ffd 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -563,7 +563,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Plug out all", "拔出所有"), ("True color (4:4:4)", "全彩模式(4:4:4)"), ("Enable blocking user input", "允許封鎖使用者輸入"), - ("id_input_tip", "您可以輸入 ID、IP、或網域名稱+端口號(<網域名稱>:<端口號>)。\n如果您要存取位於其他伺服器上的設備,請在ID之後添加伺服器地址(@<伺服器地址>?key=<金鑰>)\n例如:9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=\n要存取公共伺服器上的設備,請輸入\"@public\",不需輸入金鑰。\n\n如果您想要在第一次連線時,強制使用中繼連接,請在 ID 的末尾添加 \"/r\",例如,\"9123456234/r\"。"), + ("id_input_tip", "您可以輸入 ID、IP、或網域名稱+通訊埠號(<網域名稱>:<通訊埠號>)。\n如果您要存取位於其他伺服器上的設備,請在ID之後添加伺服器地址(@<伺服器地址>?key=<金鑰>)\n例如:9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=\n要存取公共伺服器上的設備,請輸入\"@public\",不需輸入金鑰。\n\n如果您想要在第一次連線時,強制使用中繼連接,請在 ID 的末尾添加 \"/r\",例如,\"9123456234/r\"。"), ("privacy_mode_impl_mag_tip", "模式 1"), ("privacy_mode_impl_virtual_display_tip", "模式 2"), ("Enter privacy mode", "進入隱私模式"), @@ -602,7 +602,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("no_need_privacy_mode_no_physical_displays_tip", "沒有物理螢幕,沒必要使用隱私模式。"), ("Follow remote cursor", "跟隨遠端游標"), ("Follow remote window focus", "跟隨遠端視窗焦點"), - ("default_proxy_tip", "預設代理協定及端口為 Socks5 和 1080"), + ("default_proxy_tip", "預設代理協定及通訊埠為 Socks5 和 1080"), ("no_audio_input_device_tip", "未找到音訊輸入裝置"), ("Incoming", "連入"), ("Outgoing", "連出"), From fd62751cb830d75b1b1395bc923527104352d3e9 Mon Sep 17 00:00:00 2001 From: BennyBeat Date: Thu, 3 Oct 2024 07:02:24 +0000 Subject: [PATCH 255/541] Improved Catalan translation (#9546) * Improved Catalan translation Improved Catalan translation * Update ca.rs Improved Catalan translation --- src/lang/ca.rs | 926 ++++++++++++++++++++++++------------------------- 1 file changed, 463 insertions(+), 463 deletions(-) diff --git a/src/lang/ca.rs b/src/lang/ca.rs index ef53608de827..4331e16be6ef 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -2,651 +2,651 @@ lazy_static::lazy_static! { pub static ref T: std::collections::HashMap<&'static str, &'static str> = [ ("Status", "Estat"), - ("Your Desktop", "El teu escriptori"), - ("desk_tip", "Pots accedir al teu escriptori amb aquest ID i contrasenya."), + ("Your Desktop", "Aquest ordinador"), + ("desk_tip", "Es pot accedir a aquest equip mitjançant les credencials:"), ("Password", "Contrasenya"), - ("Ready", "Llest"), - ("Established", "Establert"), - ("connecting_status", "Connexió a la xarxa RustDesk en progrés..."), - ("Enable service", "Habilita el servei"), - ("Start service", "Inicia el servei"), - ("Service is running", "El servei s'està executant"), - ("Service is not running", "El servei no s'està executant"), - ("not_ready_status", "No està llest. Comprova la teva connexió"), - ("Control Remote Desktop", "Controlar escriptori remot"), - ("Transfer file", "Transferir arxiu"), - ("Connect", "Connectar"), + ("Ready", "Preparat."), + ("Established", "S'ha establert."), + ("connecting_status", "S'està connectant a la xarxa de RustDesk..."), + ("Enable service", "Habilita el servei."), + ("Start service", "Inicia el servei."), + ("Service is running", "El servei s'està executant."), + ("Service is not running", "El servei no s'està executant."), + ("not_ready_status", "No disponible. Verifiqueu la connexió"), + ("Control Remote Desktop", "Dispositiu remot"), + ("Transfer file", "Transfereix fitxers"), + ("Connect", "Connecta"), ("Recent sessions", "Sessions recents"), - ("Address book", "Directori"), + ("Address book", "Llibreta d'adreces"), ("Confirmation", "Confirmació"), ("TCP tunneling", "Túnel TCP"), - ("Remove", "Eliminar"), + ("Remove", "Suprimeix"), ("Refresh random password", "Actualitza la contrasenya aleatòria"), - ("Set your own password", "Estableix la teva contrasenya"), + ("Set your own password", "Establiu la vostra contrasenya"), ("Enable keyboard/mouse", "Habilita el teclat/ratolí"), - ("Enable clipboard", "Habilita el portapapers"), - ("Enable file transfer", "Habilita la transferència d'arxius"), + ("Enable clipboard", "Habilita el porta-retalls"), + ("Enable file transfer", "Habilita la transferència de fitxers"), ("Enable TCP tunneling", "Habilita el túnel TCP"), ("IP Whitelisting", "Adreces IP admeses"), - ("ID/Relay Server", "Servidor ID/Relay"), + ("ID/Relay Server", "ID/Repetidor del Servidor"), ("Import server config", "Importa la configuració del servidor"), ("Export Server Config", "Exporta la configuració del servidor"), - ("Import server configuration successfully", "Configuració del servidor importada amb èxit"), - ("Export server configuration successfully", "Configuració del servidor exportada con èxit"), - ("Invalid server configuration", "Configuració del servidor incorrecta"), - ("Clipboard is empty", "El portapapers està buit"), + ("Import server configuration successfully", "S'ha importat la configuració del servidor correctament"), + ("Export server configuration successfully", "S'ha exportat la configuració del servidor correctament"), + ("Invalid server configuration", "Configuració del servidor no vàlida"), + ("Clipboard is empty", "El porta-retalls és buit"), ("Stop service", "Atura el servei"), - ("Change ID", "Canviar ID"), - ("Your new ID", "La teva nova ID"), - ("length %min% to %max%", "Ha de tenir entre %min% i %max% caràcters"), - ("starts with a letter", "comença amb una lletra"), - ("allowed characters", "caràcters permesos"), - ("id_change_tip", "Només pots utilitzar caràcters a-z, A-Z, 0-9 e _ (guionet baix). El primer caràcter ha de ser a-z o A-Z. La longitut ha d'estar entre 6 i 16 caràcters."), + ("Change ID", "Canvia la ID"), + ("Your new ID", "Identificador nou"), + ("length %min% to %max%", "Entre %min% i %max% caràcters"), + ("starts with a letter", "Comença amb una lletra"), + ("allowed characters", "Caràcters admesos"), + ("id_change_tip", "Els caràcters admesos són: a-z, A-Z, 0-9, _ (guió baix). El primer caràcter ha de ser a-z/A-Z, i una mida de 6 a 16 caràcters."), ("Website", "Lloc web"), - ("About", "Sobre"), - ("Slogan_tip", ""), - ("Privacy Statement", "Declaració de privacitat"), + ("About", "Quant al RustDesk"), + ("Slogan_tip", "Fet de tot cor dins d'aquest món caòtic!\nTraducció: Benet R. i Camps (BennyBeat)."), + ("Privacy Statement", "Declaració de privadesa"), ("Mute", "Silencia"), - ("Build Date", "Data de creació"), + ("Build Date", "Data de compilació"), ("Version", "Versió"), ("Home", "Inici"), ("Audio Input", "Entrada d'àudio"), ("Enhancements", "Millores"), - ("Hardware Codec", "Còdec de hardware"), - ("Adaptive bitrate", "Tasa de bits adaptativa"), - ("ID Server", "Servidor de IDs"), - ("Relay Server", "Servidor Relay"), - ("API Server", "Servidor API"), + ("Hardware Codec", "Codificació per maquinari"), + ("Adaptive bitrate", "Taxa de bits adaptativa"), + ("ID Server", "ID del servidor"), + ("Relay Server", "Repetidor del servidor"), + ("API Server", "Clau API del servidor"), ("invalid_http", "ha de començar amb http:// o https://"), - ("Invalid IP", "IP incorrecta"), - ("Invalid format", "Format incorrecte"), - ("server_not_support", "Encara no és compatible amb el servidor"), + ("Invalid IP", "IP no vàlida"), + ("Invalid format", "Format no vàlid"), + ("server_not_support", "Encara no suportat pel servidor"), ("Not available", "No disponible"), - ("Too frequent", "Massa comú"), + ("Too frequent", "Massa freqüent"), ("Cancel", "Cancel·la"), - ("Skip", "Salta"), - ("Close", "Tanca"), - ("Retry", "Reintenta"), + ("Skip", "Omet"), + ("Close", "Surt"), + ("Retry", "Torna a provar"), ("OK", "D'acord"), - ("Password Required", "Es necessita la contrasenya"), - ("Please enter your password", "Introdueix la teva contrasenya"), + ("Password Required", "Contrasenya requerida"), + ("Please enter your password", "Inseriu la contrasenya"), ("Remember password", "Recorda la contrasenya"), - ("Wrong Password", "Contrasenya incorrecta"), - ("Do you want to enter again?", "Vols tornar a entrar?"), + ("Wrong Password", "Contrasenya no vàlida"), + ("Do you want to enter again?", "Voleu tornar a provar?"), ("Connection Error", "Error de connexió"), ("Error", "Error"), - ("Reset by the peer", "Reestablert pel peer"), - ("Connecting...", "Connectant..."), - ("Connection in progress. Please wait.", "Connexió en procés. Espera."), - ("Please try 1 minute later", "Torna a provar-ho d'aquí un minut"), - ("Login Error", "Error d'inici de sessió"), - ("Successful", "Exitós"), - ("Connected, waiting for image...", "Connectant, esperant imatge..."), + ("Reset by the peer", "Restablert pel client"), + ("Connecting...", "S'està connectant..."), + ("Connection in progress. Please wait.", "S'està connectant. Espereu..."), + ("Please try 1 minute later", "Torneu a provar en 1 minut"), + ("Login Error", "Error d'accés"), + ("Successful", "Correcte"), + ("Connected, waiting for image...", "S'ha connectat; en espera de rebre la imatge..."), ("Name", "Nom"), ("Type", "Tipus"), ("Modified", "Modificat"), - ("Size", "Grandària"), - ("Show Hidden Files", "Mostra arxius ocults"), + ("Size", "Mida"), + ("Show Hidden Files", "Mostra els fitxers ocults"), ("Receive", "Rep"), ("Send", "Envia"), - ("Refresh File", "Actualitza el fitxer"), + ("Refresh File", "Actualitza"), ("Local", "Local"), ("Remote", "Remot"), - ("Remote Computer", "Ordinador remot"), - ("Local Computer", "Ordinador local"), - ("Confirm Delete", "Confirma l'eliminació"), - ("Delete", "Elimina"), + ("Remote Computer", "Dispositiu remot"), + ("Local Computer", "Aquest ordinador"), + ("Confirm Delete", "Confirmació de supressió"), + ("Delete", "Suprimeix"), ("Properties", "Propietats"), ("Multi Select", "Selecció múltiple"), - ("Select All", "Selecciona-ho tot"), - ("Unselect All", "Deselecciona-ho tot"), + ("Select All", "Seleciona-ho tot"), + ("Unselect All", "Desselecciona-ho tot"), ("Empty Directory", "Carpeta buida"), ("Not an empty directory", "No és una carpeta buida"), - ("Are you sure you want to delete this file?", "Segur que vols eliminar aquest fitxer?"), - ("Are you sure you want to delete this empty directory?", "Segur que vols eliminar aquesta carpeta buida?"), - ("Are you sure you want to delete the file of this directory?", "Segur que vols eliminar aquest fitxer d'aquesta car`peta?"), - ("Do this for all conflicts", "Fes això per a tots els conflictes"), - ("This is irreversible!", "Això és irreversible!"), - ("Deleting", "Eliminant"), + ("Are you sure you want to delete this file?", "Segur que voleu suprimir aquest fitxer?"), + ("Are you sure you want to delete this empty directory?", "Segur que voleu suprimir aquesta carpeta buida?"), + ("Are you sure you want to delete the file of this directory?", "Segur que voleu suprimir el fitxer d'aquesta carpeta?"), + ("Do this for all conflicts", "Aplica aquesta acció per a tots els conflictes"), + ("This is irreversible!", "Aquesta acció no es pot desfer!"), + ("Deleting", "S'està suprimint"), ("files", "fitxers"), - ("Waiting", "Esperant"), - ("Finished", "Acabat"), + ("Waiting", "En espera"), + ("Finished", "Ha finalitzat"), ("Speed", "Velocitat"), ("Custom Image Quality", "Qualitat d'imatge personalitzada"), ("Privacy mode", "Mode privat"), - ("Block user input", "Bloqueja l'entrada d'usuari"), - ("Unblock user input", "Desbloqueja l'entrada d'usuari"), + ("Block user input", "Bloca el control a l'usuari"), + ("Unblock user input", "Desbloca el control a l'usuari"), ("Adjust Window", "Ajusta la finestra"), ("Original", "Original"), - ("Shrink", "Reduir"), - ("Stretch", "Estirar"), + ("Shrink", "Encongida"), + ("Stretch", "Ampliada"), ("Scrollbar", "Barra de desplaçament"), ("ScrollAuto", "Desplaçament automàtic"), ("Good image quality", "Bona qualitat d'imatge"), - ("Balanced", "Equilibrat"), + ("Balanced", "Equilibrada"), ("Optimize reaction time", "Optimitza el temps de reacció"), - ("Custom", "Personalitzat"), + ("Custom", "Personalitzada"), ("Show remote cursor", "Mostra el cursor remot"), - ("Show quality monitor", "Mostra la qualitat del monitor"), - ("Disable clipboard", "Deshabilita el portapapers"), - ("Lock after session end", "Bloqueja després del final de la sessió"), + ("Show quality monitor", "Mostra la informació de flux"), + ("Disable clipboard", "Inhabilita el porta-retalls"), + ("Lock after session end", "Bloca en finalitzar la sessió"), ("Insert", "Insereix"), - ("Insert Lock", "Insereix bloqueig"), + ("Insert Lock", "Bloca"), ("Refresh", "Actualitza"), - ("ID does not exist", "L'ID no existeix"), - ("Failed to connect to rendezvous server", "No es pot connectar al servidor rendezvous"), - ("Please try later", "Prova-ho més tard"), - ("Remote desktop is offline", "L'escriptori remot està desconecctat"), + ("ID does not exist", "Aquesta ID no existeix"), + ("Failed to connect to rendezvous server", "Ha fallat en connectar al servidor assignat"), + ("Please try later", "Proveu més tard"), + ("Remote desktop is offline", "El dispositiu remot està desconnectat"), ("Key mismatch", "La clau no coincideix"), - ("Timeout", "Temps esgotat"), - ("Failed to connect to relay server", "No es pot connectar al servidor de relay"), - ("Failed to connect via rendezvous server", "No es pot connectar a través del servidor de rendezvous"), - ("Failed to connect via relay server", "No es pot connectar a través del servidor de relay"), - ("Failed to make direct connection to remote desktop", "No s'ha pogut establir una connexió directa amb l'escriptori remot"), - ("Set Password", "Configura la contrasenya"), - ("OS Password", "contrasenya del sistema operatiu"), - ("install_tip", ""), - ("Click to upgrade", "Clica per a actualitzar"), - ("Click to download", "Clica per a descarregar"), - ("Click to update", "Clica per a refrescar"), - ("Configure", "Configurr"), - ("config_acc", ""), - ("config_screen", "Configurar pantalla"), - ("Installing ...", "Instal·lant ..."), + ("Timeout", "S'ha exhaurit el temps"), + ("Failed to connect to relay server", "Ha fallat en connectar amb el repetidor del servidor"), + ("Failed to connect via rendezvous server", "Ha fallat en connectar mitjançant el servidor assignat"), + ("Failed to connect via relay server", "Ha fallat en connectar mitjançant el repetidor del servidor"), + ("Failed to make direct connection to remote desktop", "Ha fallat la connexió directa amb el dispositiu remot"), + ("Set Password", "Establiu una contrasenya"), + ("OS Password", "Contrasenya del sistema"), + ("install_tip", "En alguns casos és possible que el RustDesk no funcioni correctament per les restriccions UAC («User Account Control»; Control de comptes d'usuari). Per evitar aquest problema, instal·leu el RustDesk al vostre sistema."), + ("Click to upgrade", "Feu clic per a actualitzar"), + ("Click to download", "Feu clic per a baixar"), + ("Click to update", "Feu clic per a actualitzar"), + ("Configure", "Configura"), + ("config_acc", "Per a poder controlar el dispositiu remotament, faciliteu al RustDesk els permisos d'accessibilitat."), + ("config_screen", "Per a poder controlar el dispositiu remotament, faciliteu al RustDesk els permisos de gravació de pantalla."), + ("Installing ...", "S'està instal·lant..."), ("Install", "Instal·la"), ("Installation", "Instal·lació"), - ("Installation Path", "Ruta d'instal·lació"), - ("Create start menu shortcuts", "Crear accessos directes al menú d'inici"), - ("Create desktop icon", "Crear icona d'escriptori"), - ("agreement_tip", ""), - ("Accept and Install", "Acceptar i instal·la"), + ("Installation Path", "Ruta de la instal·lació"), + ("Create start menu shortcuts", "Crea una drecera al menú d'inici"), + ("Create desktop icon", "Crea una icona a l'escriptori"), + ("agreement_tip", "En iniciar la instal·lació, esteu acceptant l'acord de llicència d'usuari."), + ("Accept and Install", "Accepta i instal·la"), ("End-user license agreement", "Acord de llicència d'usuari final"), - ("Generating ...", "Generant ..."), - ("Your installation is lower version.", "La teva instal·lació és una versión inferior."), - ("not_close_tcp_tip", ""), - ("Listening ...", "Escoltant..."), - ("Remote Host", "Hoste remot"), + ("Generating ...", "S'està generant..."), + ("Your installation is lower version.", "La instal·lació actual és una versió inferior"), + ("not_close_tcp_tip", "No tanqueu aquesta finestra mentre utilitzeu el túnel"), + ("Listening ...", "S'està escoltant..."), + ("Remote Host", "Amfitrió remot"), ("Remote Port", "Port remot"), ("Action", "Acció"), ("Add", "Afegeix"), ("Local Port", "Port local"), - ("Local Address", "Adreça Local"), + ("Local Address", "Adreça local"), ("Change Local Port", "Canvia el port local"), - ("setup_server_tip", ""), - ("Too short, at least 6 characters.", "Massa curt, almenys 6 caràcters."), - ("The confirmation is not identical.", "La confirmació no coincideix."), + ("setup_server_tip", "Per a connexions més ràpides o privades, configureu el vostre servidor"), + ("Too short, at least 6 characters.", "Massa curt. Són necessaris almenys 6 caràcters."), + ("The confirmation is not identical.", "Les contrasenyes no coincideixen."), ("Permissions", "Permisos"), ("Accept", "Accepta"), - ("Dismiss", "Cancel·la"), + ("Dismiss", "Ignora"), ("Disconnect", "Desconnecta"), - ("Enable file copy and paste", "Permet copiar i enganxar fitxers"), + ("Enable file copy and paste", "Habilita la còpia i enganxament de fitxers"), ("Connected", "Connectat"), - ("Direct and encrypted connection", "Connexió directa i xifrada"), - ("Relayed and encrypted connection", "Connexió retransmesa i xifrada"), - ("Direct and unencrypted connection", "Connexió directa i sense xifrar"), - ("Relayed and unencrypted connection", "Connexió retransmesa i sense xifrar"), - ("Enter Remote ID", "Introdueix l'ID remot"), - ("Enter your password", "Introdueix la teva contrasenya"), - ("Logging in...", "Iniciant sessió..."), + ("Direct and encrypted connection", "Connexió xifrada directa"), + ("Relayed and encrypted connection", "Connexió xifrada per repetidor"), + ("Direct and unencrypted connection", "Connexió directa sense xifratge"), + ("Relayed and unencrypted connection", "Connexió per repetidor sense xifratge"), + ("Enter Remote ID", "Inseriu la ID remota"), + ("Enter your password", "Inseriu la contrasenya"), + ("Logging in...", "S'està iniciant..."), ("Enable RDP session sharing", "Habilita l'ús compartit de sessions RDP"), ("Auto Login", "Inici de sessió automàtic"), - ("Enable direct IP access", "Habilita accés IP directe"), - ("Rename", "Renombra"), + ("Enable direct IP access", "Habilita l'accés directe per IP"), + ("Rename", "Reanomena"), ("Space", "Espai"), - ("Create desktop shortcut", "Crea accés directe a l'escriptori"), + ("Create desktop shortcut", "Crea una drecera a l'escriptori"), ("Change Path", "Canvia la ruta"), - ("Create Folder", "Crea carpeta"), - ("Please enter the folder name", "Indica el nom de la carpeta"), - ("Fix it", "Soluciona-ho"), - ("Warning", "Avís"), - ("Login screen using Wayland is not supported", "La pantalla d'inici de sessió amb Wayland no és compatible"), + ("Create Folder", "Carpeta nova"), + ("Please enter the folder name", "Inseriu el nom de la carpeta"), + ("Fix it", "Repara"), + ("Warning", "Atenció"), + ("Login screen using Wayland is not supported", "L'inici de sessió amb Wayland encara no és compatible"), ("Reboot required", "Cal reiniciar"), ("Unsupported display server", "Servidor de visualització no compatible"), ("x11 expected", "x11 necessari"), ("Port", "Port"), - ("Settings", "Ajustaments"), - ("Username", " Nom d'usuari"), - ("Invalid port", "Port incorrecte"), - ("Closed manually by the peer", "Tancat manualment pel peer"), - ("Enable remote configuration modification", "Habilitar modificació remota de configuració"), - ("Run without install", "Executa sense instal·lar"), - ("Connect via relay", "Connecta per relay"), - ("Always connect via relay", "Connecta sempre a través de relay"), - ("whitelist_tip", ""), - ("Login", "Inicia sessió"), + ("Settings", "Configuració"), + ("Username", "Nom d'usuari"), + ("Invalid port", "Port no vàlid"), + ("Closed manually by the peer", "Tancat manualment pel client"), + ("Enable remote configuration modification", "Habilita la modificació remota de la configuració"), + ("Run without install", "Inicia sense instal·lar"), + ("Connect via relay", "Connecta mitjançant un repetidor"), + ("Always connect via relay", "Connecta sempre mitjançant un repetidor"), + ("whitelist_tip", "Només les IP admeses es podran connectar"), + ("Login", "Inicia la sessió"), ("Verify", "Verifica"), ("Remember me", "Recorda'm"), ("Trust this device", "Confia en aquest dispositiu"), ("Verification code", "Codi de verificació"), - ("verification_tip", ""), - ("Logout", "Sortir"), + ("verification_tip", "S'ha enviat un codi de verificació al correu-e registrat. Inseriu-lo per a continuar amb l'inici de sessió."), + ("Logout", "Tanca la sessió"), ("Tags", "Etiquetes"), - ("Search ID", "Cerca ID"), - ("whitelist_sep", ""), - ("Add ID", "Afegir ID"), - ("Add Tag", "Afegir tag"), - ("Unselect all tags", "Deseleccionar tots els tags"), - ("Network error", "Error de xarxa"), - ("Username missed", "Nom d'usuari oblidat"), - ("Password missed", "Contrasenya oblidada"), - ("Wrong credentials", "Credencials incorrectes"), - ("The verification code is incorrect or has expired", "El codi de verificació es incorrecte o ha expirat"), - ("Edit Tag", "Editar tag"), + ("Search ID", "Cerca per ID"), + ("whitelist_sep", "Separades per coma, punt i coma, espai o una adreça per línia"), + ("Add ID", "Afegeix una ID"), + ("Add Tag", "Afegeix una etiqueta"), + ("Unselect all tags", "Desselecciona totes les etiquetes"), + ("Network error", "Error de la xarxa"), + ("Username missed", "No s'ha indicat el nom d'usuari"), + ("Password missed", "No s'ha indicat la contrasenya"), + ("Wrong credentials", "Credencials errònies"), + ("The verification code is incorrect or has expired", "El codi de verificació no és vàlid o ha caducat"), + ("Edit Tag", "Edita l'etiqueta"), ("Forget Password", "Contrasenya oblidada"), ("Favorites", "Preferits"), - ("Add to Favorites", "Afegir a preferits"), - ("Remove from Favorites", "Treure de preferits"), - ("Empty", "Buit"), - ("Invalid folder name", "Nom de carpeta incorrecte"), - ("Socks5 Proxy", "Proxy Socks5"), - ("Socks5/Http(s) Proxy", "Proxy Socks5/Http(s)"), + ("Add to Favorites", "Afegeix als preferits"), + ("Remove from Favorites", "Suprimeix dels preferits"), + ("Empty", "Buida"), + ("Invalid folder name", "Nom de carpeta no vàlid"), + ("Socks5 Proxy", "Servidor intermediari Socks5"), + ("Socks5/Http(s) Proxy", "Servidor intermediari Socks5/Http(s)"), ("Discovered", "Descobert"), - ("install_daemon_tip", ""), - ("Remote ID", "ID remot"), + ("install_daemon_tip", "Per a iniciar durant l'arrencada del sistema, heu d'instal·lar el servei."), + ("Remote ID", "ID remota"), ("Paste", "Enganxa"), - ("Paste here?", "Enganxa-ho aquí?"), - ("Are you sure to close the connection?", "Segur que vols tancar la connexió?"), - ("Download new version", "Descarrega una nova versió"), + ("Paste here?", "Voleu enganxar aquí?"), + ("Are you sure to close the connection?", "Segur que voleu finalitzar la connexió?"), + ("Download new version", "Baixa la versió nova"), ("Touch mode", "Mode tàctil"), ("Mouse mode", "Mode ratolí"), - ("One-Finger Tap", "Toca amb un dit"), - ("Left Mouse", "Ratolí esquerra"), - ("One-Long Tap", "Toc llarg"), - ("Two-Finger Tap", "Toqui amb dos dits"), + ("One-Finger Tap", "Toc amb un dit"), + ("Left Mouse", "Botó esquerre"), + ("One-Long Tap", "Toc prolongat"), + ("Two-Finger Tap", "Toc amb dos dits"), ("Right Mouse", "Botó dret"), - ("One-Finger Move", "Moviment amb un dir"), - ("Double Tap & Move", "Toca dos cops i mogui"), - ("Mouse Drag", "Arrossega amb el ratolí"), - ("Three-Finger vertically", "Tres dits verticalment"), + ("One-Finger Move", "Moviment amb un dit"), + ("Double Tap & Move", "Toc doble i moveu"), + ("Mouse Drag", "Arrossega el ratolí"), + ("Three-Finger vertically", "Tres dits en vertical"), ("Mouse Wheel", "Roda del ratolí"), ("Two-Finger Move", "Moviment amb dos dits"), ("Canvas Move", "Moviment del llenç"), - ("Pinch to Zoom", "Pessiga per fer zoom"), - ("Canvas Zoom", "Amplia el llenç"), - ("Reset canvas", "Reestableix el llenç"), - ("No permission of file transfer", "No tens permís de transferència de fitxers"), + ("Pinch to Zoom", "Pessic per escalar"), + ("Canvas Zoom", "escala del llenç"), + ("Reset canvas", "Reinici del llenç"), + ("No permission of file transfer", "Cap permís per a transferència de fitxers"), ("Note", "Nota"), ("Connection", "Connexió"), - ("Share Screen", "Comparteix la pantalla"), + ("Share Screen", "Compartició de pantalla"), ("Chat", "Xat"), ("Total", "Total"), - ("items", "ítems"), + ("items", "elements"), ("Selected", "Seleccionat"), ("Screen Capture", "Captura de pantalla"), ("Input Control", "Control d'entrada"), ("Audio Capture", "Captura d'àudio"), - ("File Connection", "Connexió d'arxius"), + ("File Connection", "Connexió de fitxer"), ("Screen Connection", "Connexió de pantalla"), - ("Do you accept?", "Acceptes?"), - ("Open System Setting", "Configuració del sistema obert"), - ("How to get Android input permission?", "Com obtenir el permís d'entrada d'Android?"), - ("android_input_permission_tip1", "Per a que un dispositiu remot controli el seu dispositiu Android amb el ratolí o tocs, cal permetre que RustDesk utilitzi el servei d' \"Accesibilitat\"."), - ("android_input_permission_tip2", "Vés a la pàgina de [Serveis instal·lats], activa el servei [RustDesk Input]."), - ("android_new_connection_tip", "S'ha rebut una nova sol·licitud de control per al dispositiu actual."), - ("android_service_will_start_tip", "Habilitar la captura de pantalla iniciarà el servei automàticament, i permetrà que altres dispositius sol·licitin una connexió des d'aquest dispositiu."), - ("android_stop_service_tip", "Tancar el servei tancarà totes les connexions establertes."), - ("android_version_audio_tip", "La versión actual de Android no admet la captura d'àudio, actualizi a Android 10 o superior."), - ("android_start_service_tip", ""), - ("android_permission_may_not_change_tip", ""), + ("Do you accept?", "Voleu acceptar?"), + ("Open System Setting", "Obre la configuració del sistema"), + ("How to get Android input permission?", "Com modificar els permisos a Android?"), + ("android_input_permission_tip1", "Per a controlar de forma remota el vostre dispositiu amb gestos o un ratolí, heu de permetre al RustDesk l'ús del servei «Accessibilitat»."), + ("android_input_permission_tip2", "A l'apartat Configuració del sistema de la pàgina següent, aneu a «Serveis baixats», i activeu el «RustDesk Input»."), + ("android_new_connection_tip", "S'ha rebut una petició nova per a controlar el vostre dispositiu."), + ("android_service_will_start_tip", "Activant «Gravació de pantalla» s'iniciarà automàticament el servei que permet a altres enviar sol·licituds de connexió cap al vostre dispositiu."), + ("android_stop_service_tip", "Tancant el servei finalitzaran automàticament les connexions en ús."), + ("android_version_audio_tip", "Aquesta versió d'Android no suporta la captura d'àudio. Actualitzeu a Android 10 o superior."), + ("android_start_service_tip", "Toqueu a «Inicia el servei» o activeu el permís «Captura de pantalla» per a iniciar el servei de compartició de pantalla."), + ("android_permission_may_not_change_tip", "Els permisos per a les connexions ja establertes poden no canviar, fins que no torneu a connectar."), ("Account", "Compte"), - ("Overwrite", "Sobreescriu"), - ("This file exists, skip or overwrite this file?", "Aquest fitxer ja existeix, ometre o sobreescriure l'arxiu?"), + ("Overwrite", "Reemplaça"), + ("This file exists, skip or overwrite this file?", "Aquest fitxer ja existeix. Voleu ometre o reemplaçar l'original?"), ("Quit", "Surt"), ("Help", "Ajuda"), ("Failed", "Ha fallat"), - ("Succeeded", "Aconseguit"), - ("Someone turns on privacy mode, exit", "Algú ha activat el mode de privacitat, surt"), + ("Succeeded", "Fet"), + ("Someone turns on privacy mode, exit", "S'ha activat el Mode privat; surt"), ("Unsupported", "No suportat"), - ("Peer denied", "Peer denegat"), - ("Please install plugins", "Instal·la els complements"), - ("Peer exit", "El peer ha sortit"), - ("Failed to turn off", "Error en apagar"), - ("Turned off", "Apagat"), + ("Peer denied", "Client denegat"), + ("Please install plugins", "Instal·leu els complements"), + ("Peer exit", "Finalitzat pel client"), + ("Failed to turn off", "Ha fallat en desactivar"), + ("Turned off", "Desactivat"), ("Language", "Idioma"), - ("Keep RustDesk background service", "Mantenir RustDesk com a servei en segon pla"), - ("Ignore Battery Optimizations", "Ignora optimizacions de la bateria"), - ("android_open_battery_optimizations_tip", ""), - ("Start on boot", "Engega en l'arrencada"), - ("Start the screen sharing service on boot, requires special permissions", "Engega el servei de captura de pantalla en l'arrencada, requereix permisos especials"), - ("Connection not allowed", "Connexió no disponible"), + ("Keep RustDesk background service", "Manté el servei del RustDesk en rerefons"), + ("Ignore Battery Optimizations", "Ignora les optimitzacions de bateria"), + ("android_open_battery_optimizations_tip", "Si voleu desactivar aquesta característica, feu-ho des de la pàgina següent de configuració del RustDesk, utilitzant l'opció relativa a «Bateria»"), + ("Start on boot", "Inicia durant l'arrencada"), + ("Start the screen sharing service on boot, requires special permissions", "Per iniciar la compartició de pantalla durant l'arrencada del sistema, calen permisos especials"), + ("Connection not allowed", "Connexió no permesa"), ("Legacy mode", "Mode heretat"), ("Map mode", "Mode mapa"), - ("Translate mode", "Mode traduit"), - ("Use permanent password", "Utilitza una contrasenya permament"), - ("Use both passwords", "Utilitza ambdues contrasenyas"), - ("Set permanent password", "Estableix una contrasenya permament"), - ("Enable remote restart", "Activa el reinici remot"), - ("Restart remote device", "Reinicia el dispositiu"), - ("Are you sure you want to restart", "Segur que vol reiniciar?"), - ("Restarting remote device", "Reiniciant el dispositiu remot"), - ("remote_restarting_tip", "Reiniciant el dispositiu remot, tanca aquest missatge i torna't a connectar amb la contrasenya."), - ("Copied", "Copiat"), + ("Translate mode", "Mode traduït"), + ("Use permanent password", "Utilitza la contrasenya permanent"), + ("Use both passwords", "Utilitza totes dues opcions"), + ("Set permanent password", "Estableix la contrasenya permanent"), + ("Enable remote restart", "Habilita el reinici remot"), + ("Restart remote device", "Reinicia el dispositiu remot"), + ("Are you sure you want to restart", "Segur que voleu reiniciar"), + ("Restarting remote device", "Reinici del dispositiu remot"), + ("remote_restarting_tip", "S'està reiniciant el dispositiu remot. Tanqueu aquest missatge i torneu a connectar amb ell mitjançant la contrasenya, un cop estigui en línia."), + ("Copied", "S'ha copiat"), ("Exit Fullscreen", "Surt de la pantalla completa"), ("Fullscreen", "Pantalla completa"), - ("Mobile Actions", "Accions mòbils"), - ("Select Monitor", "Selecciona el monitor"), - ("Control Actions", "Accions de control"), + ("Mobile Actions", "Funcions mòbils"), + ("Select Monitor", "Selecció de monitor"), + ("Control Actions", "Control de funcions"), ("Display Settings", "Configuració de pantalla"), ("Ratio", "Relació"), - ("Image Quality", "Qualitat d'imatge"), - ("Scroll Style", "Estil de desplaçament"), + ("Image Quality", "Qualitat de la imatge"), + ("Scroll Style", "Tipus de desplaçament"), ("Show Toolbar", "Mostra la barra d'eines"), ("Hide Toolbar", "Amaga la barra d'eines"), ("Direct Connection", "Connexió directa"), - ("Relay Connection", "Connexió Relay"), + ("Relay Connection", "Connexió amb repetidor"), ("Secure Connection", "Connexió segura"), - ("Insecure Connection", "Connexió insegura"), + ("Insecure Connection", "Connexió no segura"), ("Scale original", "Escala original"), ("Scale adaptive", "Escala adaptativa"), ("General", "General"), ("Security", "Seguretat"), ("Theme", "Tema"), - ("Dark Theme", "Tema Fosc"), + ("Dark Theme", "Tema fosc"), ("Light Theme", "Tema clar"), ("Dark", "Fosc"), ("Light", "Clar"), - ("Follow System", "Tema del sistema"), - ("Enable hardware codec", "Habilita el còdec per hardware"), - ("Unlock Security Settings", "Desbloqueja els ajustaments de seguretat"), + ("Follow System", "Utilitza la configuració del sistema"), + ("Enable hardware codec", "Habilita la codificació per maquinari"), + ("Unlock Security Settings", "Desbloca la configuració de seguretat"), ("Enable audio", "Habilita l'àudio"), - ("Unlock Network Settings", "Desbloqueja els ajustaments de xarxa"), + ("Unlock Network Settings", "Desbloca la configuració de la xarxa"), ("Server", "Servidor"), - ("Direct IP Access", "Accés IP Directe"), - ("Proxy", "Proxy"), + ("Direct IP Access", "Accés directe per IP"), + ("Proxy", "Servidor intermediari"), ("Apply", "Aplica"), - ("Disconnect all devices?", "Vols desconnectar tots els dispositius?"), - ("Clear", "Neteja"), + ("Disconnect all devices?", "Voleu desconnectar tots els dispositius?"), + ("Clear", "Buida"), ("Audio Input Device", "Dispositiu d'entrada d'àudio"), - ("Use IP Whitelisting", "Utilitza llista de IPs admeses"), + ("Use IP Whitelisting", "Utilitza un llistat d'IP admeses"), ("Network", "Xarxa"), - ("Pin Toolbar", "Fixa la barra d'eines"), - ("Unpin Toolbar", "Deixa de fixar la barra d'eines"), - ("Recording", "Gravant"), - ("Directory", "Directori"), - ("Automatically record incoming sessions", "Gravació automàtica de sessions entrants"), + ("Pin Toolbar", "Ancora a la barra d'eines"), + ("Unpin Toolbar", "Desancora de la barra d'eines"), + ("Recording", "Gravació"), + ("Directory", "Contactes"), + ("Automatically record incoming sessions", "Enregistrament automàtic de sessions entrants"), ("Change", "Canvia"), - ("Start session recording", "Comença la gravació de la sessió"), + ("Start session recording", "Inicia la gravació de la sessió"), ("Stop session recording", "Atura la gravació de la sessió"), ("Enable recording session", "Habilita la gravació de la sessió"), - ("Enable LAN discovery", "Habilita el descobriment de LAN"), - ("Deny LAN discovery", "Denega el descobriment de LAN"), - ("Write a message", "Escriu un missatge"), - ("Prompt", "Consulta"), - ("Please wait for confirmation of UAC...", "Espera per confirmar l'UAC..."), - ("elevated_foreground_window_tip", ""), + ("Enable LAN discovery", "Habilita el descobriment LAN"), + ("Deny LAN discovery", "Inhabilita el descobriment LAN"), + ("Write a message", "Escriviu un missatge"), + ("Prompt", "Sol·licitud"), + ("Please wait for confirmation of UAC...", "Espereu a la confirmació de l'UAC..."), + ("elevated_foreground_window_tip", "La finestra de connexió actual requereix permisos ampliats per a funcionar i, de forma temporal, no es pot utilitzar ni el teclat ni el ratolí. Demaneu a l'usuari remot que minimitzi la finestra actual, o bé que faci clic al botó Permisos ampliats de la finestra d'administració de la connexió. Per a evitar aquest problema en un futur, instal·leu el RustDesk al dispositiu remot."), ("Disconnected", "Desconnectat"), ("Other", "Altre"), - ("Confirm before closing multiple tabs", "Confirma abans de tancar múltiples pestanyes"), - ("Keyboard Settings", "Ajustaments de teclat"), - ("Full Access", "Acces complet"), - ("Screen Share", "Comparteix la pantalla"), - ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland requereix Ubuntu 21.04 o una versió superior."), - ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland requereix una versió superior de la distribución de Linux. Prova l'escriptori X11 o canvia el teu sistema operatiu."), - ("JumpLink", "Veure"), - ("Please Select the screen to be shared(Operate on the peer side).", "Selecciona la pantalla que es compartirà (Opera al costat del peer)."), - ("Show RustDesk", "Mostra RustDesk"), - ("This PC", "Aquest PC"), + ("Confirm before closing multiple tabs", "Confirma abans de tancar diverses pestanyes alhora"), + ("Keyboard Settings", "Configuració del teclat"), + ("Full Access", "Accés complet"), + ("Screen Share", "Compartició de pantalla"), + ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland requereix Ubuntu 21.04 o superior"), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland requereix una versió superior de sistema Linux per a funcionar. Proveu iniciant un entorn d'escriptori amb x11 o actualitzeu el vostre sistema operatiu."), + ("JumpLink", "Marcador"), + ("Please Select the screen to be shared(Operate on the peer side).", "Seleccioneu la pantalla que compartireu (quina serà visible al client)"), + ("Show RustDesk", "Mostra el RustDesk"), + ("This PC", "Aquest equip"), ("or", "o"), - ("Continue with", "Continur amb"), - ("Elevate", "Eleva"), - ("Zoom cursor", "Zoom del ratolí"), - ("Accept sessions via password", "Accepta sessions via contrasenya"), - ("Accept sessions via click", "Accepta sessions via clic"), - ("Accept sessions via both", "Accepta sessions via les dues opcions"), - ("Please wait for the remote side to accept your session request...", "Esperea que la part remota accepti la teva sol·licitud de sessió..."), + ("Continue with", "Continua amb"), + ("Elevate", "Permisos ampliats"), + ("Zoom cursor", "Escala del ratolí"), + ("Accept sessions via password", "Accepta les sessions mitjançant una contrasenya"), + ("Accept sessions via click", "Accepta les sessions expressament amb el ratolí"), + ("Accept sessions via both", "Accepta les sessions de totes dues formes"), + ("Please wait for the remote side to accept your session request...", "S'està esperant l'acceptació remota de la vostra connexió..."), ("One-time Password", "Contrasenya d'un sol ús"), - ("Use one-time password", "Fes servir una contrasenya d'un sol ús"), - ("One-time password length", "Caracters de la contrasenya d'un sol ús"), - ("Request access to your device", "Sol·licita l'acces al teu dispositiu"), - ("Hide connection management window", "Amaga la finestra de gestió de connexió"), - ("hide_cm_tip", ""), - ("wayland_experiment_tip", ""), - ("Right click to select tabs", "Clic dret per seleccionar les pestanyes"), - ("Skipped", "Saltat"), + ("Use one-time password", "Utilitza una contrasenya d'un sol ús"), + ("One-time password length", "Mida de la contrasenya d'un sol ús"), + ("Request access to your device", "Ha demanat connectar al vostre dispositiu"), + ("Hide connection management window", "Amaga la finestra d'administració de la connexió"), + ("hide_cm_tip", "Permet amagar la finestra només en acceptar sessions entrants sempre que s'utilitzi una contrasenya permanent"), + ("wayland_experiment_tip", "El suport per a Wayland està en fase experimental; es recomana l'ús d'x11 si us cal accés de forma desatesa."), + ("Right click to select tabs", "Feu clic amb el botó dret per a seleccionar pestanyes"), + ("Skipped", "S'ha omès"), ("Add to address book", "Afegeix a la llibreta d'adreces"), ("Group", "Grup"), ("Search", "Cerca"), - ("Closed manually by web console", "Tancat manualment amb la consola web"), + ("Closed manually by web console", "Tancat manualment per la consola web"), ("Local keyboard type", "Tipus de teclat local"), - ("Select local keyboard type", "Selecciona el tipus de teclat local"), - ("software_render_tip", ""), - ("Always use software rendering", "Fes servir sempre la renderització per software"), - ("config_input", ""), - ("config_microphone", ""), - ("request_elevation_tip", ""), - ("Wait", "Espera"), - ("Elevation Error", "Error d'elevació"), - ("Ask the remote user for authentication", "Demana autenticació a l'usuari remot"), - ("Choose this if the remote account is administrator", "Selecciona això si l'usuari remot és administrador"), - ("Transmit the username and password of administrator", "Transmet el nom d'usuari i la contrasenya de l'administrador"), - ("still_click_uac_tip", ""), - ("Request Elevation", "Demana l'elevació"), - ("wait_accept_uac_tip", ""), - ("Elevate successfully", "Elevació exitosa"), + ("Select local keyboard type", "Seleccioneu el tipus de teclat local"), + ("software_render_tip", "Si utilitzeu una gràfica Nvidia a Linux i la connexió remota es tanca immediatament en connectar, canviar al controlador lliure «Nouveau» amb renderització per programari, pot ajudar a solucionar el problema. Es requerirà en aquest cas reiniciar l'aplicació."), + ("Always use software rendering", "Utilitza sempre la renderització de programari"), + ("config_input", "Per a poder controlar el dispositiu remotament amb el teclat, faciliteu al RustDesk els permisos d'entrada necessaris."), + ("config_microphone", "Per a poder parlar remotament, faciliteu al RustDesk els permisos de gravació d'àudio necessaris."), + ("request_elevation_tip", "També, la part remota pot concedir aquests permisos de forma manual."), + ("Wait", "Espereu"), + ("Elevation Error", "Error de permisos"), + ("Ask the remote user for authentication", "Demaneu l'autenticació al client remot"), + ("Choose this if the remote account is administrator", "Trieu aquesta opció si el compte remot té permisos d'administrador"), + ("Transmit the username and password of administrator", "Indiqueu l'usuari i contrasenya de l'administrador"), + ("still_click_uac_tip", "Es requereix acceptació manual a la part remota de la finestra «UAC» del RustDesk en execució."), + ("Request Elevation", "Sol·licita els permisos"), + ("wait_accept_uac_tip", "Espereu fins que l'usuari remot accepti la finestra de diàleg de l'«UAC»."), + ("Elevate successfully", "S'han acceptat els permisos"), ("uppercase", "majúscula"), ("lowercase", "minúscula"), - ("digit", "dígit"), + ("digit", "número"), ("special character", "caràcter especial"), - ("length>=8", "longitut>=8"), - ("Weak", "Dèbil"), - ("Medium", "Mitja"), - ("Strong", "Forta"), - ("Switch Sides", "Canvia de costat"), - ("Please confirm if you want to share your desktop?", "Confirma que vols compartir el teu escriptori?"), + ("length>=8", "mida>=8"), + ("Weak", "Feble"), + ("Medium", "Acceptable"), + ("Strong", "Segura"), + ("Switch Sides", "Inverteix la connexió"), + ("Please confirm if you want to share your desktop?", "Realment voleu que es controli aquest equip?"), ("Display", "Pantalla"), - ("Default View Style", "Estil de visualització predeterminat"), - ("Default Scroll Style", "Estil de desplaçament predeterminat"), - ("Default Image Quality", "Qualitat d'imatge predeterminada"), - ("Default Codec", "Còdec predeterminat"), - ("Bitrate", "Ratio de bits"), + ("Default View Style", "Estil de vista per defecte"), + ("Default Scroll Style", "Estil de desplaçament per defecte"), + ("Default Image Quality", "Qualitat de la imatge per defecte"), + ("Default Codec", "Còdec per defecte"), + ("Bitrate", "Taxa de bits"), ("FPS", "FPS"), - ("Auto", "Auto"), - ("Other Default Options", "Altres opcions predeterminades"), - ("Voice call", "Trucada de veu"), - ("Text chat", "Xat de text"), - ("Stop voice call", "Penja la trucada de veu"), - ("relay_hint_tip", ""), - ("Reconnect", "Reconecta"), - ("Codec", "Còdec"), + ("Auto", "Automàtic"), + ("Other Default Options", "Altres opcions per defecte"), + ("Voice call", "Trucada"), + ("Text chat", "Xat"), + ("Stop voice call", "Penja la trucada"), + ("relay_hint_tip", "Quan no sigui possible la connexió directa, podeu provar mitjançant un repetidor. Addicionalment, si voleu que l'ús d'un repetidor sigui la primera opció per defecte, podeu afegir el sufix «/r» a la ID, o seleccionar l'opció «Connecta sempre mitjançant un repetidor» si ja existeix una fitxa amb aquesta ID a la pestanya de connexions recents."), + ("Reconnect", "Torna a connectar"), + ("Codec", "Còdec"), ("Resolution", "Resolució"), - ("No transfers in progress", "Sense transferències en curs"), - ("Set one-time password length", "Selecciona la longitud de la contrasenya d'un sol ús"), - ("RDP Settings", "Configuració RDP"), - ("Sort by", "Ordena per"), - ("New Connection", "Nova connexió"), + ("No transfers in progress", "Cap transferència iniciada"), + ("Set one-time password length", "Mida de la contrasenya d'un sol ús"), + ("RDP Settings", "Opcions de connexió RDP"), + ("Sort by", "Organitza per"), + ("New Connection", "Connexió nova"), ("Restore", "Restaura"), ("Minimize", "Minimitza"), - ("Maximize", "Maximtiza"), - ("Your Device", "El teu dispositiu"), - ("empty_recent_tip", ""), - ("empty_favorite_tip", ""), - ("empty_lan_tip", ""), - ("empty_address_book_tip", ""), - ("eg: admin", "p.ex.: admin"), - ("Empty Username", "Usuari buit"), + ("Maximize", "Maximitza"), + ("Your Device", "Aquest dispositiu"), + ("empty_recent_tip", "No s'ha trobat cap sessió recent!\nS'afegiran automàticament les connexions que realitzeu."), + ("empty_favorite_tip", "No heu afegit cap dispositiu aquí!\nPodeu afegir dispositius favorits en qualsevol moment."), + ("empty_lan_tip", "No s'ha trobat cap dispositiu proper."), + ("empty_address_book_tip", "Sembla que no teniu cap dispositiu a la vostra llista d'adreces."), + ("eg: admin", "p. ex.:admin"), + ("Empty Username", "Nom d'usuari buit"), ("Empty Password", "Contrasenya buida"), - ("Me", "Jo"), - ("identical_file_tip", ""), - ("show_monitors_tip", ""), - ("View Mode", "Tipus de visualització"), - ("login_linux_tip", ""), - ("verify_rustdesk_password_tip", ""), - ("remember_account_tip", ""), - ("os_account_desk_tip", ""), - ("OS Account", ""), - ("another_user_login_title_tip", ""), - ("another_user_login_text_tip", ""), - ("xorg_not_found_title_tip", ""), - ("xorg_not_found_text_tip", ""), - ("no_desktop_title_tip", ""), - ("no_desktop_text_tip", ""), - ("No need to elevate", "No cal elevar permisos"), - ("System Sound", "Sistema de so"), - ("Default", "Predeterminat"), - ("New RDP", "Nou RDP"), - ("Fingerprint", "Empremta digital"), - ("Copy Fingerprint", "Copia l'emprenta digital"), - ("no fingerprints", "sense emprentes"), - ("Select a peer", "Selecciona un peer"), - ("Select peers", "Selecciona diversos peers"), + ("Me", "Vós"), + ("identical_file_tip", "Aquest fitxer és idèntic al del client."), + ("show_monitors_tip", "Mostra les pantalles a la barra d'eines"), + ("View Mode", "Mode espectador"), + ("login_linux_tip", "És necessari que inicieu prèviament sessió amb un entorn d'escriptori x11 habilitat"), + ("verify_rustdesk_password_tip", "Verifica la contrasenya del RustDesk"), + ("remember_account_tip", "Recorda aquest compte"), + ("os_account_desk_tip", "S'utilitza aquest compte per iniciar la sessió al sistema remot i habilitar el mode sense cap pantalla connectada"), + ("OS Account", "Compte d'usuari"), + ("another_user_login_title_tip", "Altre usuari ha iniciat ja una sessió"), + ("another_user_login_text_tip", "Desconnecta"), + ("xorg_not_found_title_tip", "No s'ha trobat l'entorn Xorg"), + ("xorg_not_found_text_tip", "Instal·leu el Xorg"), + ("no_desktop_title_tip", "Cap escriptori disponible"), + ("no_desktop_text_tip", "Instal·leu l'entorn d'escriptori GNOME"), + ("No need to elevate", "No calen permisos ampliats"), + ("System Sound", "So del sistema"), + ("Default", "per defecte"), + ("New RDP", "Connexió RDP nova"), + ("Fingerprint", "Empremta"), + ("Copy Fingerprint", "Copia l'empremta"), + ("no fingerprints", "Cap empremta"), + ("Select a peer", "Seleccioneu un client"), + ("Select peers", "Seleccioneu els clients"), ("Plugins", "Complements"), ("Uninstall", "Desinstal·la"), ("Update", "Actualitza"), ("Enable", "Activa"), ("Disable", "Desactiva"), ("Options", "Opcions"), - ("resolution_original_tip", ""), - ("resolution_fit_local_tip", ""), - ("resolution_custom_tip", ""), - ("Collapse toolbar", "Col·lapsa la barra d'etiquetes"), - ("Accept and Elevate", "Accepta i eleva"), - ("accept_and_elevate_btn_tooltip", ""), - ("clipboard_wait_response_timeout_tip", ""), + ("resolution_original_tip", "Resolució original"), + ("resolution_fit_local_tip", "Ajusta la resolució local"), + ("resolution_custom_tip", "Resolució personalitzada"), + ("Collapse toolbar", "Minimitza la barra d'eines"), + ("Accept and Elevate", "Accepta i permet"), + ("accept_and_elevate_btn_tooltip", "Accepta la connexió i permet els permisos elevats UAC."), + ("clipboard_wait_response_timeout_tip", "S'ha esgotat el temps d'espera amb la resposta de còpia."), ("Incoming connection", "Connexió entrant"), ("Outgoing connection", "Connexió sortint"), - ("Exit", "Tanca"), + ("Exit", "Surt"), ("Open", "Obre"), - ("logout_tip", ""), + ("logout_tip", "Segur que voleu desconnectar?"), ("Service", "Servei"), ("Start", "Inicia"), ("Stop", "Atura"), - ("exceed_max_devices", ""), + ("exceed_max_devices", "Heu assolit el nombre màxim de dispositius administrables."), ("Sync with recent sessions", "Sincronitza amb les sessions recents"), - ("Sort tags", "Ordena per etiquetes"), - ("Open connection in new tab", "Obre la connexió en una nova pestanya"), - ("Move tab to new window", "Mou la pestanya a una nova finestra"), + ("Sort tags", "Ordena les etiquetes"), + ("Open connection in new tab", "Obre la connexió en una pestanya nova"), + ("Move tab to new window", "Mou la pestanya a una finestra nova"), ("Can not be empty", "No pot estar buit"), ("Already exists", "Ja existeix"), ("Change Password", "Canvia la contrasenya"), - ("Refresh Password", "Refresca la contrasenya"), + ("Refresh Password", "Actualitza la contrasenya"), ("ID", "ID"), - ("Grid View", "Visualització de graella"), - ("List View", "Visualització de llista"), + ("Grid View", "Disposició de graella"), + ("List View", "Disposició de llista"), ("Select", "Selecciona"), - ("Toggle Tags", "Activa/desactiva les etiquetes"), - ("pull_ab_failed_tip", ""), - ("push_ab_failed_tip", ""), - ("synced_peer_readded_tip", ""), + ("Toggle Tags", "Habilita les etiquetes"), + ("pull_ab_failed_tip", "Ha fallat en actualitzar la llista de contactes"), + ("push_ab_failed_tip", "Ha fallat en actualitzar la llista amb el servidor"), + ("synced_peer_readded_tip", "Els dispositius que es troben a la llista de sessions recents se sincronitzaran novament a la llista de contactes."), ("Change Color", "Canvia el color"), - ("Primary Color", "Color primari"), + ("Primary Color", "Color principal"), ("HSV Color", "Color HSV"), - ("Installation Successful!", "Instal·lació correcta!"), - ("Installation failed!", "Ha fallat la instal·lació!"), - ("Reverse mouse wheel", "Canvia l'orientació de la roda del ratolí"), + ("Installation Successful!", "S'ha instal·lat correctament"), + ("Installation failed!", "Ha fallat la instal·lació"), + ("Reverse mouse wheel", "Inverteix la roda del ratolí"), ("{} sessions", "{} sessions"), - ("scam_title", ""), - ("scam_text1", ""), - ("scam_text2", ""), - ("Don't show again", "No ho mostris més"), - ("I Agree", "Accepta"), - ("Decline", "Rebutja"), - ("Timeout in minutes", "Desconexió en minuts"), - ("auto_disconnect_option_tip", ""), - ("Connection failed due to inactivity", "Connexió fallada per inactivitat"), - ("Check for software update on startup", "Revisa les actualitzacions de software en iniciar"), - ("upgrade_rustdesk_server_pro_to_{}_tip", ""), - ("pull_group_failed_tip", ""), + ("scam_title", "Podríeu ser víctima d'una ESTAFA!"), + ("scam_text1", "Si cap persona qui NO coneixeu NI CONFIEU us demanés l'ús del RustDesk, no continueu i talleu la comunicació immediatament."), + ("scam_text2", "Habitualment solen ser atacants intentant fer-se amb els vostres diners o informació privada."), + ("Don't show again", "No tornis a mostrar"), + ("I Agree", "Accepto"), + ("Decline", "No accepto"), + ("Timeout in minutes", "Temps d'espera en minuts"), + ("auto_disconnect_option_tip", "Tanca automàticament les sessions entrants per inactivitat de l'usuari"), + ("Connection failed due to inactivity", "Ha fallat la connexió per inactivitat"), + ("Check for software update on startup", "Cerca actualitzacions en iniciar"), + ("upgrade_rustdesk_server_pro_to_{}_tip", "Actualitzeu el RustDesk Server Pro a la versió {} o superior!"), + ("pull_group_failed_tip", "Ha fallat en actualitzar el grup"), ("Filter by intersection", "Filtra per intersecció"), - ("Remove wallpaper during incoming sessions", "Amaga el fons de pantalla en les connexions entrants"), + ("Remove wallpaper during incoming sessions", "Inhabilita el fons d'escriptori durant la sessió entrant"), ("Test", "Prova"), - ("display_is_plugged_out_msg", ""), - ("No displays", "Sense pantalles"), - ("Open in new window", "Obre en una nova finestra"), - ("Show displays as individual windows", "Mostra les pantalles com finestres individuals"), - ("Use all my displays for the remote session", "Fes servir totes les meves pantalles per la sessió remota"), - ("selinux_tip", ""), + ("display_is_plugged_out_msg", "El monitor està desconnectat; canvieu primer al monitor principal."), + ("No displays", "Cap monitor"), + ("Open in new window", "Obre en una finestra nova"), + ("Show displays as individual windows", "Mostra cada monitor com una finestra individual"), + ("Use all my displays for the remote session", "Utilitza tots els meus monitors per a la connexió remota"), + ("selinux_tip", "SELinux està activat al vostre dispositiu, la qual cosa evita que el RustDesk funcioni correctament com a equip controlable."), ("Change view", "Canvia la vista"), - ("Big tiles", "Títols grans"), - ("Small tiles", "Títols petits"), + ("Big tiles", "Mosaic gran"), + ("Small tiles", "Mosaic petit"), ("List", "Llista"), ("Virtual display", "Pantalla virtual"), - ("Plug out all", "Desconnectar tots"), + ("Plug out all", "Desconnecta-ho tot"), ("True color (4:4:4)", "Color real (4:4:4)"), - ("Enable blocking user input", "Activa el bloqueig d'entrada d'usuari"), - ("id_input_tip", ""), - ("privacy_mode_impl_mag_tip", ""), - ("privacy_mode_impl_virtual_display_tip", ""), - ("Enter privacy mode", "Entra al mode de privacitat"), - ("Exit privacy mode", "Surt del mode de privacitat"), - ("idd_not_support_under_win10_2004_tip", ""), - ("input_source_1_tip", ""), - ("input_source_2_tip", ""), - ("Swap control-command key", "Canvia la tecla de control"), - ("swap-left-right-mouse", ""), + ("Enable blocking user input", "Bloca el control de l'usuari amb els dispositius d'entrada"), + ("id_input_tip", "Evita que l'usuari pugui interactuar p. ex. amb el teclat o ratolí"), + ("privacy_mode_impl_mag_tip", "Mode 1"), + ("privacy_mode_impl_virtual_display_tip", "Mode 2"), + ("Enter privacy mode", "Inicia el Mode privat"), + ("Exit privacy mode", "Surt del Mode privat"), + ("idd_not_support_under_win10_2004_tip", "El controlador indirecte de pantalla no està suportat; es requereix Windows 10 versió 2004 o superior."), + ("input_source_1_tip", "Font d'entrada 1"), + ("input_source_2_tip", "Font d'entrada 2"), + ("Swap control-command key", "Canvia el comportament de la tecla Control"), + ("swap-left-right-mouse", "Alterna el comportament dels botons esquerre-dret del ratolí"), ("2FA code", "Codi 2FA"), ("More", "Més"), - ("enable-2fa-title", ""), - ("enable-2fa-desc", ""), - ("wrong-2fa-code", ""), - ("enter-2fa-title", ""), - ("Email verification code must be 6 characters.", "El codi de verificació de l'email ha de tenir 6 caràcters."), - ("2FA code must be 6 digits.", "El codi 2FA ha de tenir 6 digits."), - ("Multiple Windows sessions found", "Multiples sessions de Windows trobades"), - ("Please select the session you want to connect to", "Selecciona la sessió a la què et vols connectar"), - ("powered_by_me", ""), - ("outgoing_only_desk_tip", ""), - ("preset_password_warning", ""), + ("enable-2fa-title", "Habilita el mètode d'autenticació de factor doble"), + ("enable-2fa-desc", "Configureu ara el vostre autenticador. Podeu utilitzar una aplicació com 2fast, FreeOTP, MultiOTP, Microsoft o Google Authenticator al vostre telèfon o escriptori.\n\nEscanegeu el codi QR amb l'aplicació i escriviu els caràcters resultants per habilitar l'autenticació de factor doble."), + ("wrong-2fa-code", "Codi 2FA no vàlid. Verifiqueu el que heu escrit i també que la configuració horària sigui correcta"), + ("enter-2fa-title", "Autenticació de factor doble"), + ("Email verification code must be 6 characters.", "El codi de verificació de correu-e són 6 caràcters"), + ("2FA code must be 6 digits.", "El codi de verificació 2FA haurien de ser almenys 6 dígits"), + ("Multiple Windows sessions found", "S'han trobat múltiples sessions en ús del Windows"), + ("Please select the session you want to connect to", "Indiqueu amb quina sessió voleu connectar"), + ("powered_by_me", "Amb la tecnologia de RustDesk"), + ("outgoing_only_desk_tip", "Aquesta és una versió personalitzada.\nPodeu connectar amb altres dispositius, però no s'accepten connexions d'entrada cap el vostre dispositiu."), + ("preset_password_warning", "Aquesta versió personalitzada té una contrasenya preestablerta. Qualsevol persona que la conegui pot tenir accés total al vostre dispositiu. Si no és el comportament desitjat, desinstal·leu aquest programa immediatament."), ("Security Alert", "Alerta de seguretat"), - ("My address book", "La meva llibreta d'adreces"), + ("My address book", "Llibreta d'adreces"), ("Personal", "Personal"), ("Owner", "Propietari"), - ("Set shared password", "Establir la contrasenya compartida"), - ("Exist in", "Existeix en"), + ("Set shared password", "Establiu una contrasenya compartida"), + ("Exist in", "Existeix a"), ("Read-only", "Només lectura"), ("Read/Write", "Lectura/Escriptura"), ("Full Control", "Control total"), - ("share_warning_tip", ""), - ("Everyone", "Tots"), - ("ab_web_console_tip", ""), - ("allow-only-conn-window-open-tip", ""), - ("no_need_privacy_mode_no_physical_displays_tip", ""), - ("Follow remote cursor", "Segueix el cursor remot"), - ("Follow remote window focus", "Segueix el focus de la finestra remota"), - ("default_proxy_tip", ""), - ("no_audio_input_device_tip", ""), + ("share_warning_tip", "Els camps a continuació estan compartits i són visibles a d'altres."), + ("Everyone", "Tothom"), + ("ab_web_console_tip", "Més a la consola web"), + ("allow-only-conn-window-open-tip", "Permet la connexió només si la finestra del RustDesk està activa"), + ("no_need_privacy_mode_no_physical_displays_tip", "Cap monitor físic. No cal l'ús del Mode privat"), + ("Follow remote cursor", "Segueix al cursor remot"), + ("Follow remote window focus", "Segueix el focus remot de la finestra activa"), + ("default_proxy_tip", "El protocol per defecte és Socks5 al port 1080"), + ("no_audio_input_device_tip", "No s'ha trobat cap dispositiu d'àudio."), ("Incoming", "Entrant"), ("Outgoing", "Sortint"), - ("Clear Wayland screen selection", "Neteja la selecció de pantalla Wayland"), - ("clear_Wayland_screen_selection_tip", ""), - ("confirm_clear_Wayland_screen_selection_tip", ""), - ("android_new_voice_call_tip", ""), - ("texture_render_tip", ""), - ("Use texture rendering", "Fes servir la renderització de textures"), + ("Clear Wayland screen selection", "Neteja la pantalla de selecció Wayland"), + ("clear_Wayland_screen_selection_tip", "En netejar la finestra de selecció, podreu tornar a triar quina pantalla compartir."), + ("confirm_clear_Wayland_screen_selection_tip", "Segur que voleu netejar la pantalla de selecció del Wayland"), + ("android_new_voice_call_tip", "S'ha rebut una petició de trucada entrant. Si accepteu, la font d'àudio canviarà a comunicació per veu."), + ("texture_render_tip", "Utilitzeu aquesta opció per suavitzar la imatge. Desactiveu-ho si trobeu cap problema amb el renderitzat"), + ("Use texture rendering", "Utilitza la renderització de textures"), ("Floating window", "Finestra flotant"), - ("floating_window_tip", ""), - ("Keep screen on", "Deixa la pantalla encesa"), + ("floating_window_tip", "Ajuda a mantenir el servei del RustDesk en rerefons"), + ("Keep screen on", "Manté la pantalla activa"), ("Never", "Mai"), - ("During controlled", "Mentre estigui controlat"), - ("During service is on", "Mentre el servei estigui encés"), - ("Capture screen using DirectX", "Captura de pantalla utilitzant DirectX"), + ("During controlled", "Durant la connexió"), + ("During service is on", "Mentre el servei està actiu"), + ("Capture screen using DirectX", "Captura utilitzant el DirectX"), ("Back", "Enrere"), ("Apps", "Aplicacions"), - ("Volume up", "Puja el volum"), - ("Volume down", "Baixa el volum"), - ("Power", "Engega"), - ("Telegram bot", "Bot de Telegram"), - ("enable-bot-tip", ""), - ("enable-bot-desc", ""), - ("cancel-2fa-confirm-tip", ""), - ("cancel-bot-confirm-tip", ""), - ("About RustDesk", ""), - ("Send clipboard keystrokes", ""), - ("network_error_tip", ""), - ("Unlock with PIN", ""), - ("Requires at least {} characters", ""), - ("Wrong PIN", ""), - ("Set PIN", ""), - ("Enable trusted devices", ""), - ("Manage trusted devices", ""), - ("Platform", ""), - ("Days remaining", ""), - ("enable-trusted-devices-tip", ""), - ("Parent directory", ""), - ("Resume", ""), - ("Invalid file name", ""), - ("one-way-file-transfer-tip", ""), - ("Authentication Required", ""), - ("Authenticate", ""), - ("web_id_input_tip", ""), + ("Volume up", "Volum amunt"), + ("Volume down", "Volum avall"), + ("Power", "Encesa"), + ("Telegram bot", "Bot del Telegram"), + ("enable-bot-tip", "Si habiliteu aquesta característica, podreu rebre el codi 2FA mitjançant el vostre bot. També funciona com a notificador de la connexió."), + ("enable-bot-desc", "1. Obriu un xat amb @BotFather.\n2. Envieu l'ordre \"/newbot\". Rebreu un testimoni en acompletar aquest pas.\n3. Inicieu una conversa amb el vostre bot nou que acabeu de crear, enviant un missatge que comenci amb (\"/\"), com ara \"/hello\" per a activar-lo.\n"),"), + ("cancel-2fa-confirm-tip", "Segur que voleu cancel·lar l'autenticació 2FA?"), + ("cancel-bot-confirm-tip", "Segur que voleu cancel·lar el bot de Telegram?"), + ("About RustDesk", "Quant al RustDesk"), + ("Send clipboard keystrokes", "Envia les pulsacions de tecles del porta-retalls"), + ("network_error_tip", "Verifiqueu la vostra connexió a Internet i torneu a provar"), + ("Unlock with PIN", "Desbloca amb PIN"), + ("Requires at least {} characters", "Són necessaris almenys {} caràcters"), + ("Wrong PIN", "PIN no vàlid"), + ("Set PIN", "Definiu un codi PIN"), + ("Enable trusted devices", "Habilita els dispositius de confiança"), + ("Manage trusted devices", "Administra els dispositius de confiança"), + ("Platform", "Platforma"), + ("Days remaining", "Dies restants"), + ("enable-trusted-devices-tip", "Omet l'autenticació de factor doble (2FA) als dispositius de confiança"), + ("Parent directory", "Carpeta pare"), + ("Resume", "Continua"), + ("Invalid file name", "Nom de fitxer no vàlid"), + ("one-way-file-transfer-tip", "One-way file transfer is enabled on the controlled side."), + ("Authentication Required", "Autenticació requerida"), + ("Authenticate", "Autentica"), + ("web_id_input_tip", "Podeu inserir el número ID al propi servidor; l'accés directe per IP no és compatible amb el client web.\nSi voleu accedir a un dispositiu d'un altre servidor, afegiu l'adreça del servidor, com ara @?key= (p. ex.\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nSi voleu accedir a un dispositiu en un servidor públic, no cal que inseriu la clau pública «@» per al servidor públic."), ].iter().cloned().collect(); } From 306dd77b8109995a9cd64654cc1a0b1dfa232ff8 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Thu, 3 Oct 2024 15:34:40 +0800 Subject: [PATCH 256/541] fix https://github.com/rustdesk/rustdesk/issues/9537 --- res/DEBIAN/prerm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/DEBIAN/prerm b/res/DEBIAN/prerm index f68be3c7e36f..938424a8b3e6 100755 --- a/res/DEBIAN/prerm +++ b/res/DEBIAN/prerm @@ -14,7 +14,7 @@ case $1 in rm /etc/systemd/system/rustdesk.service /usr/lib/systemd/system/rustdesk.service || true # workaround temp dev build between 1.1.9 and 1.2.0 - ubuntuVersion=$(grep -oP 'VERSION_ID="\K[\d]+' /etc/os-release | bc -l) + ubuntuVersion=$(grep -oP 'VERSION_ID="\K[\d]+' /etc/os-release | awk '{print $1}') waylandSupportVersion=21 if [ "$ubuntuVersion" != "" ] && [ "$ubuntuVersion" -ge "$waylandSupportVersion" ] then From c4d0b02478e866cfb2402679c07ec1ecbc2ba3b3 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Thu, 3 Oct 2024 15:38:55 +0800 Subject: [PATCH 257/541] fix ci --- src/lang/ca.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lang/ca.rs b/src/lang/ca.rs index 4331e16be6ef..cd262589da79 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -626,7 +626,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Power", "Encesa"), ("Telegram bot", "Bot del Telegram"), ("enable-bot-tip", "Si habiliteu aquesta característica, podreu rebre el codi 2FA mitjançant el vostre bot. També funciona com a notificador de la connexió."), - ("enable-bot-desc", "1. Obriu un xat amb @BotFather.\n2. Envieu l'ordre \"/newbot\". Rebreu un testimoni en acompletar aquest pas.\n3. Inicieu una conversa amb el vostre bot nou que acabeu de crear, enviant un missatge que comenci amb (\"/\"), com ara \"/hello\" per a activar-lo.\n"),"), + ("enable-bot-desc", "1. Obriu un xat amb @BotFather.\n2. Envieu l'ordre \"/newbot\". Rebreu un testimoni en acompletar aquest pas.\n3. Inicieu una conversa amb el vostre bot nou que acabeu de crear, enviant un missatge que comenci amb (\"/\"), com ara \"/hello\" per a activar-lo.\n"), ("cancel-2fa-confirm-tip", "Segur que voleu cancel·lar l'autenticació 2FA?"), ("cancel-bot-confirm-tip", "Segur que voleu cancel·lar el bot de Telegram?"), ("About RustDesk", "Quant al RustDesk"), From dec3cde9b32dd3fa5166bc8b0dd228f7249a260a Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Thu, 3 Oct 2024 17:56:21 +0800 Subject: [PATCH 258/541] fix: deb, build, prerm (#9552) Signed-off-by: fufesou --- res/DEBIAN/prerm | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/res/DEBIAN/prerm b/res/DEBIAN/prerm index 938424a8b3e6..baef2e2e202c 100755 --- a/res/DEBIAN/prerm +++ b/res/DEBIAN/prerm @@ -14,15 +14,10 @@ case $1 in rm /etc/systemd/system/rustdesk.service /usr/lib/systemd/system/rustdesk.service || true # workaround temp dev build between 1.1.9 and 1.2.0 - ubuntuVersion=$(grep -oP 'VERSION_ID="\K[\d]+' /etc/os-release | awk '{print $1}') - waylandSupportVersion=21 - if [ "$ubuntuVersion" != "" ] && [ "$ubuntuVersion" -ge "$waylandSupportVersion" ] + serverUser=$(ps -ef | grep -E 'rustdesk +--server' | grep -v 'sudo ' | awk '{print $1}' | head -1) + if [ "$serverUser" != "" ] && [ "$serverUser" != "root" ] then - serverUser=$(ps -ef | grep -E 'rustdesk +--server' | grep -v 'sudo ' | awk '{print $1}' | head -1) - if [ "$serverUser" != "" ] && [ "$serverUser" != "root" ] - then - systemctl --machine=${serverUser}@.host --user stop rustdesk >/dev/null 2>&1 || true - fi + systemctl --machine=${serverUser}@.host --user stop rustdesk >/dev/null 2>&1 || true fi rm /usr/lib/systemd/user/rustdesk.service >/dev/null 2>&1 || true fi From 3a97b63e95e2f48ac399884c832b9e3948da4bab Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Fri, 4 Oct 2024 22:05:38 +0800 Subject: [PATCH 259/541] fix: windows, multi-window, move between monitors (#9562) Signed-off-by: fufesou --- flutter/pubspec.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index 5a4a74ed6769..6210b5399c81 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -335,7 +335,7 @@ packages: description: path: "." ref: HEAD - resolved-ref: ad889c35b896435eb7c828e200db2fb6f41d2c3b + resolved-ref: "0842f44d8644911f65a6b78be22474af0f8a9349" url: "https://github.com/rustdesk-org/rustdesk_desktop_multi_window" source: git version: "0.1.0" From 9ea09c1515c882cccd6061ce09b2e106de0282c7 Mon Sep 17 00:00:00 2001 From: Ibnul Mutaki <36250619+cacing69@users.noreply.github.com> Date: Sat, 5 Oct 2024 09:54:17 +0700 Subject: [PATCH 260/541] Update id (Indonesia) translation on some part (#9566) * update id trans * update id trans --- src/lang/id.rs | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/lang/id.rs b/src/lang/id.rs index 5d5d06a662fb..c16243aedd2f 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -569,23 +569,23 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enter privacy mode", "Masuk mode privasi"), ("Exit privacy mode", "Keluar mode privasi"), ("idd_not_support_under_win10_2004_tip", "Driver grafis yang Anda gunakan tidak kompatibel dengan versi Windows Anda dan memerlukan Windows 10 versi 2004 atau yang lebih baru"), - ("input_source_1_tip", ""), - ("input_source_2_tip", ""), - ("Swap control-command key", ""), - ("swap-left-right-mouse", ""), - ("2FA code", ""), - ("More", ""), - ("enable-2fa-title", ""), - ("enable-2fa-desc", ""), - ("wrong-2fa-code", ""), - ("enter-2fa-title", ""), - ("Email verification code must be 6 characters.", ""), - ("2FA code must be 6 digits.", ""), - ("Multiple Windows sessions found", ""), - ("Please select the session you want to connect to", ""), - ("powered_by_me", ""), - ("outgoing_only_desk_tip", ""), - ("preset_password_warning", ""), + ("input_source_1_tip", "Sumber input 1"), + ("input_source_2_tip", "Sumber input 2"), + ("Swap control-command key", "Menukar tombol control-command"), + ("swap-left-right-mouse", "Tukar fungsi tombol kiri dan kanan pada mouse"), + ("2FA code", "Kode 2FA"), + ("More", "Lainnya"), + ("enable-2fa-title", "Aktifkan autentikasi 2FA"), + ("enable-2fa-desc", "Silakan atur autentikator Anda sekarang. Anda dapat menggunakan aplikasi autentikator seperti Authy, Microsoft Authenticator, atau Google Authenticator di ponsel atau desktop Anda\n\nPindai kode QR dengan aplikasi Anda dan masukkan kode yang ditampilkan oleh aplikasi untuk mengaktifkan autentikasi 2FA."), + ("wrong-2fa-code", "Tidak dapat memverifikasi kode. Pastikan bahwa kode dan pengaturan waktu lokal sudah sesuai"), + ("enter-2fa-title", "Autentikasi dua faktor"), + ("Email verification code must be 6 characters.", "Kode verifikasi email harus terdiri dari 6 karakter."), + ("2FA code must be 6 digits.", "Kode 2FA harus terdiri dari 6 digit."), + ("Multiple Windows sessions found", "Terdapat beberapa sesi Windows"), + ("Please select the session you want to connect to", "Silakan pilih sesi yang ingin Anda sambungkan."), + ("powered_by_me", "Didukung oleh RustDesk"), + ("outgoing_only_desk_tip", "Ini adalah edisi yang sudah kustomisasi.\nAnda dapat terhubung ke perangkat lain, tetapi perangkat lain tidak dapat terhubung ke perangkat Anda."), + ("preset_password_warning", "Edisi yang dikustomisasi ini dilengkapi dengan kata sandi bawaan. Siapa pun yang mengetahui kata sandi ini dapat memperoleh kontrol penuh atas perangkat Anda. Jika Anda tidak mengharapkan ini, segera hapus pemasangan aplikasi tersebut."), ("Security Alert", ""), ("My address book", ""), ("Personal", ""), From ba832362a75f7c306688df462f109a6a65dd08f2 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Sun, 6 Oct 2024 08:32:04 +0800 Subject: [PATCH 261/541] fix: installed, copy&paste, special format (#9570) Signed-off-by: fufesou --- src/ipc.rs | 1 + src/server/clipboard_service.rs | 1 + src/ui_cm_interface.rs | 1 + 3 files changed, 3 insertions(+) diff --git a/src/ipc.rs b/src/ipc.rs index 9815fdb748bb..c7243d821708 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -121,6 +121,7 @@ pub struct ClipboardNonFile { pub height: i32, // message.proto: ClipboardFormat pub format: i32, + pub special_name: String, } #[cfg(not(any(target_os = "android", target_os = "ios")))] diff --git a/src/server/clipboard_service.rs b/src/server/clipboard_service.rs index 55ebbc2f2ec5..3aadb3ad5dd2 100644 --- a/src/server/clipboard_service.rs +++ b/src/server/clipboard_service.rs @@ -117,6 +117,7 @@ impl Handler { format: ClipboardFormat::from_i32(c.format) .unwrap_or(ClipboardFormat::Text) .into(), + special_name: c.special_name, ..Default::default() }) .collect(), diff --git a/src/ui_cm_interface.rs b/src/ui_cm_interface.rs index 549798a51728..c34e15e26c84 100644 --- a/src/ui_cm_interface.rs +++ b/src/ui_cm_interface.rs @@ -512,6 +512,7 @@ impl IpcTaskRunner { width: c.width, height: c.height, format: c.format.value(), + special_name: c.special_name, }); } allow_err!(self.stream.send(&Data::ClipboardNonFile(Some(("".to_owned(), main_data)))).await); From e7353be0cd0747febada01216a20aeba1ee52ba3 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Sun, 6 Oct 2024 09:25:13 +0800 Subject: [PATCH 262/541] fix https://github.com/flathub/flathub/pull/5670#issuecomment-2395067890 --- src/platform/linux.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/platform/linux.rs b/src/platform/linux.rs index 9c549423045d..3f0860f868e5 100644 --- a/src/platform/linux.rs +++ b/src/platform/linux.rs @@ -610,8 +610,17 @@ pub fn get_env_var(k: &str) -> String { } } +fn is_flatpak() -> bool { + std::env::var("FLATPAK_SANDBOX_DIR").is_ok() || + std::env::var("FLATPAK_ID").is_ok() || + std::env::var("FLATPAK_SESSION_BUS_ADDRESS").is_ok() +} + // Headless is enabled, always return true. pub fn is_prelogin() -> bool { + if is_flatpak() { + return false; + } let n = get_active_userid().len(); n < 4 && n > 1 } From 560c1effe81c20180ce2fc96b3d86902531cc723 Mon Sep 17 00:00:00 2001 From: 21pages Date: Sun, 6 Oct 2024 16:20:37 +0800 Subject: [PATCH 263/541] remove mobile web setting, remove web/ios relay server setting (#9575) Signed-off-by: 21pages --- .../desktop/pages/desktop_setting_page.dart | 5 +- flutter/lib/mobile/widgets/dialog.dart | 17 +++-- flutter/lib/web/settings_page.dart | 73 +------------------ 3 files changed, 13 insertions(+), 82 deletions(-) diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 041b5569ed51..ebe55ca191b5 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -1451,8 +1451,9 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin { children: [ Obx(() => _LabeledTextField(context, 'ID Server', idController, idErrMsg.value, enabled, secure)), - Obx(() => _LabeledTextField(context, 'Relay Server', - relayController, relayErrMsg.value, enabled, secure)), + if (!isWeb) + Obx(() => _LabeledTextField(context, 'Relay Server', + relayController, relayErrMsg.value, enabled, secure)), Obx(() => _LabeledTextField(context, 'API Server', apiController, apiErrMsg.value, enabled, secure)), _LabeledTextField( diff --git a/flutter/lib/mobile/widgets/dialog.dart b/flutter/lib/mobile/widgets/dialog.dart index 391bec669d58..2d17f3b5438c 100644 --- a/flutter/lib/mobile/widgets/dialog.dart +++ b/flutter/lib/mobile/widgets/dialog.dart @@ -205,14 +205,15 @@ void showServerSettingsWithValue( ) ] + [ - TextFormField( - controller: relayCtrl, - decoration: InputDecoration( - labelText: translate('Relay Server'), - errorText: relayServerMsg.value.isEmpty - ? null - : relayServerMsg.value), - ) + if (isAndroid) + TextFormField( + controller: relayCtrl, + decoration: InputDecoration( + labelText: translate('Relay Server'), + errorText: relayServerMsg.value.isEmpty + ? null + : relayServerMsg.value), + ) ] + [ TextFormField( diff --git a/flutter/lib/web/settings_page.dart b/flutter/lib/web/settings_page.dart index cbd79a718ecd..1cf23ecf9f88 100644 --- a/flutter/lib/web/settings_page.dart +++ b/flutter/lib/web/settings_page.dart @@ -1,23 +1,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_hbb/desktop/pages/desktop_setting_page.dart'; -import 'package:flutter_hbb/mobile/pages/scan_page.dart'; -import 'package:flutter_hbb/mobile/pages/settings_page.dart'; -import 'package:provider/provider.dart'; - -import '../../common.dart'; -import '../../common/widgets/login.dart'; -import '../../models/model.dart'; class WebSettingsPage extends StatelessWidget { const WebSettingsPage({Key? key}) : super(key: key); @override Widget build(BuildContext context) { - if (isWebDesktop) { - return _buildDesktopButton(context); - } else { - return _buildMobileMenu(context); - } + return _buildDesktopButton(context); } Widget _buildDesktopButton(BuildContext context) { @@ -34,64 +23,4 @@ class WebSettingsPage extends StatelessWidget { }, ); } - - Widget _buildMobileMenu(BuildContext context) { - Provider.of(context); - return PopupMenuButton( - tooltip: "", - icon: const Icon(Icons.more_vert), - itemBuilder: (context) { - return (isIOS - ? [ - const PopupMenuItem( - value: "scan", - child: Icon(Icons.qr_code_scanner, color: Colors.black), - ) - ] - : >[]) + - [ - PopupMenuItem( - value: "server", - child: Text(translate('ID/Relay Server')), - ) - ] + - [ - PopupMenuItem( - value: "login", - child: Text(gFFI.userModel.userName.value.isEmpty - ? translate("Login") - : '${translate("Logout")} (${gFFI.userModel.userName.value})'), - ) - ] + - [ - PopupMenuItem( - value: "about", - child: Text(translate('About RustDesk')), - ) - ]; - }, - onSelected: (value) { - if (value == 'server') { - showServerSettings(gFFI.dialogManager); - } - if (value == 'about') { - showAbout(gFFI.dialogManager); - } - if (value == 'login') { - if (gFFI.userModel.userName.value.isEmpty) { - loginDialog(); - } else { - logOutConfirmDialog(); - } - } - if (value == 'scan') { - Navigator.push( - context, - MaterialPageRoute( - builder: (BuildContext context) => ScanPage(), - ), - ); - } - }); - } } From 83aba804d0fc87d1ffec6063e0d391d6c1a302f8 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Mon, 7 Oct 2024 00:01:14 +0800 Subject: [PATCH 264/541] fix: web fullscreen (#9577) Signed-off-by: fufesou --- .../lib/desktop/widgets/remote_toolbar.dart | 21 +++++++ flutter/lib/models/native_model.dart | 8 +++ flutter/lib/models/state_model.dart | 63 +++++++++++++------ flutter/lib/models/web_model.dart | 6 ++ 4 files changed, 80 insertions(+), 18 deletions(-) diff --git a/flutter/lib/desktop/widgets/remote_toolbar.dart b/flutter/lib/desktop/widgets/remote_toolbar.dart index 2d20d5931c9e..d0fbc46731d8 100644 --- a/flutter/lib/desktop/widgets/remote_toolbar.dart +++ b/flutter/lib/desktop/widgets/remote_toolbar.dart @@ -436,6 +436,7 @@ class _RemoteToolbarState extends State { shadowColor: MyTheme.color(context).shadow, borderRadius: borderRadius, child: _DraggableShowHide( + id: widget.id, sessionId: widget.ffi.sessionId, dragging: _dragging, fractionX: _fractionX, @@ -2218,6 +2219,7 @@ class RdoMenuButton extends StatelessWidget { } class _DraggableShowHide extends StatefulWidget { + final String id; final SessionID sessionId; final RxDouble fractionX; final RxBool dragging; @@ -2229,6 +2231,7 @@ class _DraggableShowHide extends StatefulWidget { const _DraggableShowHide({ Key? key, + required this.id, required this.sessionId, required this.fractionX, required this.dragging, @@ -2364,6 +2367,24 @@ class _DraggableShowHideState extends State<_DraggableShowHide> { ), ))), ), + if (isWebDesktop) + Obx(() { + if (show.isTrue) { + return Offstage(); + } else { + return TextButton( + onPressed: () => closeConnection(id: widget.id), + child: Tooltip( + message: translate('Close'), + child: Icon( + Icons.close, + size: iconSize, + color: _ToolbarTheme.redColor, + ), + ), + ); + } + }) ], ); return TextButtonTheme( diff --git a/flutter/lib/models/native_model.dart b/flutter/lib/models/native_model.dart index b99cf2e7fb82..c8d5085e8973 100644 --- a/flutter/lib/models/native_model.dart +++ b/flutter/lib/models/native_model.dart @@ -48,6 +48,12 @@ class PlatformFFI { static get isMain => instance._appType == kAppTypeMain; + static String getByName(String name, [String arg = '']) { + return ''; + } + + static void setByName(String name, [String value = '']) {} + static Future getVersion() async { PackageInfo packageInfo = await PackageInfo.fromPlatform(); return packageInfo.version; @@ -276,4 +282,6 @@ class PlatformFFI { void syncAndroidServiceAppDirConfigPath() { invokeMethod(AndroidChannel.kSyncAppDirConfigPath, _dir); } + + void setFullscreenCallback(void Function(bool) fun) {} } diff --git a/flutter/lib/models/state_model.dart b/flutter/lib/models/state_model.dart index a1b5fc736e9b..62f92db12188 100644 --- a/flutter/lib/models/state_model.dart +++ b/flutter/lib/models/state_model.dart @@ -2,6 +2,7 @@ import 'package:desktop_multi_window/desktop_multi_window.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/common.dart'; import 'package:get/get.dart'; +import 'native_model.dart' if (dart.library.html) 'web_model.dart'; import '../consts.dart'; import './platform_model.dart'; @@ -73,27 +74,47 @@ class StateGlobal { if (_fullscreen.value != v) { _fullscreen.value = v; _showTabBar.value = !_fullscreen.value; - refreshResizeEdgeSize(); - print( - "fullscreen: $fullscreen, resizeEdgeSize: ${_resizeEdgeSize.value}"); - _windowBorderWidth.value = fullscreen.isTrue ? 0 : kWindowBorderWidth; - if (procWnd) { - final wc = WindowController.fromWindowId(windowId); - wc.setFullscreen(_fullscreen.isTrue).then((_) { - // https://github.com/leanflutter/window_manager/issues/131#issuecomment-1111587982 - if (isWindows && !v) { - Future.delayed(Duration.zero, () async { - final frame = await wc.getFrame(); - final newRect = Rect.fromLTWH( - frame.left, frame.top, frame.width + 1, frame.height + 1); - await wc.setFrame(newRect); - }); - } - }); + if (isWebDesktop) { + procFullscreenWeb(); + } else { + procFullscreenNative(procWnd); } } } + procFullscreenWeb() { + final isFullscreen = PlatformFFI.getByName('fullscreen') == 'Y'; + String fullscreenValue = ''; + if (isFullscreen && _fullscreen.isFalse) { + fullscreenValue = 'N'; + } else if (!isFullscreen && fullscreen.isTrue) { + fullscreenValue = 'Y'; + } + if (fullscreenValue.isNotEmpty) { + PlatformFFI.setByName('fullscreen', fullscreenValue); + } + } + + procFullscreenNative(bool procWnd) { + refreshResizeEdgeSize(); + print("fullscreen: $fullscreen, resizeEdgeSize: ${_resizeEdgeSize.value}"); + _windowBorderWidth.value = fullscreen.isTrue ? 0 : kWindowBorderWidth; + if (procWnd) { + final wc = WindowController.fromWindowId(windowId); + wc.setFullscreen(_fullscreen.isTrue).then((_) { + // https://github.com/leanflutter/window_manager/issues/131#issuecomment-1111587982 + if (isWindows && _fullscreen.isFalse) { + Future.delayed(Duration.zero, () async { + final frame = await wc.getFrame(); + final newRect = Rect.fromLTWH( + frame.left, frame.top, frame.width + 1, frame.height + 1); + await wc.setFrame(newRect); + }); + } + }); + } + } + refreshResizeEdgeSize() => _resizeEdgeSize.value = fullscreen.isTrue ? kFullScreenEdgeSize : isMaximized.isTrue @@ -112,7 +133,13 @@ class StateGlobal { _inputSource = bind.mainGetInputSource(); } - StateGlobal._(); + StateGlobal._() { + if (isWebDesktop) { + platformFFI.setFullscreenCallback((v) { + _fullscreen.value = v; + }); + } + } static final StateGlobal instance = StateGlobal._(); } diff --git a/flutter/lib/models/web_model.dart b/flutter/lib/models/web_model.dart index ff0d1b806dad..a5740af61a13 100644 --- a/flutter/lib/models/web_model.dart +++ b/flutter/lib/models/web_model.dart @@ -166,4 +166,10 @@ class PlatformFFI { // just for compilation void syncAndroidServiceAppDirConfigPath() {} + + void setFullscreenCallback(void Function(bool) fun) { + context["onFullscreenChanged"] = (bool v) { + fun(v); + }; + } } From 839e8180e03037b23936fc9bca883a41515c7d96 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Mon, 7 Oct 2024 12:00:56 +0800 Subject: [PATCH 265/541] refact: web, no minimize btn on fullscreen (#9578) Signed-off-by: fufesou --- flutter/lib/desktop/widgets/remote_toolbar.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/lib/desktop/widgets/remote_toolbar.dart b/flutter/lib/desktop/widgets/remote_toolbar.dart index d0fbc46731d8..e7b4d4d761fd 100644 --- a/flutter/lib/desktop/widgets/remote_toolbar.dart +++ b/flutter/lib/desktop/widgets/remote_toolbar.dart @@ -2340,7 +2340,7 @@ class _DraggableShowHideState extends State<_DraggableShowHide> { ), ), )), - if (!isMacOS) + if (!isMacOS && !isWebDesktop) Obx(() => Offstage( offstage: isFullscreen.isFalse, child: TextButton( From cc860b290603fc913c3196999a1ffec407a2083d Mon Sep 17 00:00:00 2001 From: rustdesk Date: Mon, 7 Oct 2024 15:26:41 +0800 Subject: [PATCH 266/541] https://github.com/flathub/flathub/pull/5670#issuecomment-2395337173 --- src/platform/linux.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/platform/linux.rs b/src/platform/linux.rs index 3f0860f868e5..08cf0fb9a901 100644 --- a/src/platform/linux.rs +++ b/src/platform/linux.rs @@ -611,9 +611,7 @@ pub fn get_env_var(k: &str) -> String { } fn is_flatpak() -> bool { - std::env::var("FLATPAK_SANDBOX_DIR").is_ok() || - std::env::var("FLATPAK_ID").is_ok() || - std::env::var("FLATPAK_SESSION_BUS_ADDRESS").is_ok() + std::path::PathBuf::from("/.flatpak-info").exists() } // Headless is enabled, always return true. From e06f456bbd3241c20993fa4603a0c8e6ca6c9bdb Mon Sep 17 00:00:00 2001 From: Dmitry Beskov <43372966+besdar@users.noreply.github.com> Date: Mon, 7 Oct 2024 14:55:25 +0400 Subject: [PATCH 267/541] manifest updates from the Flathub's PR (#9581) Signed-off-by: dmitry <43372966+besdar@users.noreply.github.com> --- .github/workflows/flutter-build.yml | 4 +- flatpak/com.rustdesk.RustDesk.metainfo.xml | 40 +++++++++++++++++ flatpak/rustdesk.json | 50 +++++++++++++--------- flatpak/xdotool.json | 15 ------- 4 files changed, 72 insertions(+), 37 deletions(-) create mode 100644 flatpak/com.rustdesk.RustDesk.metainfo.xml delete mode 100644 flatpak/xdotool.json diff --git a/.github/workflows/flutter-build.yml b/.github/workflows/flutter-build.yml index dd21515ba547..989aee65f92a 100644 --- a/.github/workflows/flutter-build.yml +++ b/.github/workflows/flutter-build.yml @@ -1951,8 +1951,8 @@ jobs: wget # flatpak deps flatpak --user remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo - flatpak --user install -y flathub org.freedesktop.Platform/${{ matrix.job.arch }}/23.08 - flatpak --user install -y flathub org.freedesktop.Sdk/${{ matrix.job.arch }}/23.08 + flatpak --user install -y flathub org.freedesktop.Platform/${{ matrix.job.arch }}/24.08 + flatpak --user install -y flathub org.freedesktop.Sdk/${{ matrix.job.arch }}/24.08 # package pushd flatpak git clone https://github.com/flathub/shared-modules.git --depth=1 diff --git a/flatpak/com.rustdesk.RustDesk.metainfo.xml b/flatpak/com.rustdesk.RustDesk.metainfo.xml new file mode 100644 index 000000000000..12842fb8abe0 --- /dev/null +++ b/flatpak/com.rustdesk.RustDesk.metainfo.xml @@ -0,0 +1,40 @@ + + + com.rustdesk.RustDesk + RustDesk + com.rustdesk.RustDesk.desktop + CC0-1.0 + AGPL-3.0-only + RustDesk + An open-source remote desktop application designed for self-hosting, as an alternative to TeamViewer. + +

    + RustDesk is a full-featured open source remote control alternative for self-hosting and security with minimal configuration. +

    +
      +
    • Works on Windows, macOS, Linux, iOS, Android, Web.
    • +
    • Supports VP8 / VP9 / AV1 software codecs, and H264 / H265 hardware codecs.
    • +
    • Own your data, easily set up self-hosting solution on your infrastructure.
    • +
    • P2P connection with end-to-end encryption based on NaCl.
    • +
    • No administrative privileges or installation needed for Windows, elevate priviledge locally or from remote on demand.
    • +
    • We like to keep things simple and will strive to make simpler where possible.
    • +
    +

    + For self-hosting setup instructions please go to our home page. +

    +
    + + Utility + + + + https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png + + + https://rustdesk.com + https://github.com/rustdesk/rustdesk/issues + + + + +
    \ No newline at end of file diff --git a/flatpak/rustdesk.json b/flatpak/rustdesk.json index 6d7acb5b89ca..366f83f5b406 100644 --- a/flatpak/rustdesk.json +++ b/flatpak/rustdesk.json @@ -1,19 +1,36 @@ { "id": "com.rustdesk.RustDesk", "runtime": "org.freedesktop.Platform", - "runtime-version": "23.08", + "runtime-version": "24.08", "sdk": "org.freedesktop.Sdk", "command": "rustdesk", - "icon": "share/icons/hicolor/scalable/apps/rustdesk.svg", + "rename-desktop-file": "rustdesk.desktop", + "rename-icon": "rustdesk", + "cleanup": [ + "/include", + "/lib/pkgconfig", + "/share/gtk-doc" + ], "modules": [ "shared-modules/libappindicator/libappindicator-gtk3-12.10.json", - "xdotool.json", { - "name": "pam", - "buildsystem": "simple", - "build-commands": [ - "./configure --disable-selinux --prefix=/app && make -j4 install" + "name": "xdotool", + "no-autogen": true, + "make-install-args": [ + "PREFIX=${FLATPAK_DEST}" ], + "sources": [ + { + "type": "archive", + "url": "https://github.com/jordansissel/xdotool/releases/download/v3.20211022.1/xdotool-3.20211022.1.tar.gz", + "sha256": "96f0facfde6d78eacad35b91b0f46fecd0b35e474c03e00e30da3fdd345f9ada" + } + ] + }, + { + "name": "pam", + "buildsystem": "autotools", + "config-opts": [ "--disable-selinux" ], "sources": [ { "type": "archive", @@ -26,32 +43,25 @@ "name": "rustdesk", "buildsystem": "simple", "build-commands": [ - "bsdtar -zxvf rustdesk.deb", - "tar -xvf ./data.tar.xz", - "cp -r ./usr/* /app/", + "bsdtar -Oxf rustdesk.deb data.tar.xz | bsdtar -xf -", + "cp -r usr/* /app/", "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-link.desktop /app/share/applications/com.rustdesk.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", - "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" + "install -Dm644 com.rustdesk.RustDesk.metainfo.xml /app/share/metainfo/com.rustdesk.RustDesk.metainfo.xml" ], - "cleanup": ["/include", "/lib/pkgconfig", "/share/gtk-doc"], "sources": [ { "type": "file", - "path": "./rustdesk.deb" + "path": "rustdesk.deb" }, { "type": "file", - "path": "../res/scalable.svg" + "path": "com.rustdesk.RustDesk.metainfo.xml" } ] } ], "finish-args": [ "--share=ipc", - "--socket=x11", "--socket=fallback-x11", "--socket=wayland", "--share=network", @@ -60,4 +70,4 @@ "--socket=pulseaudio", "--talk-name=org.freedesktop.Flatpak" ] -} +} \ No newline at end of file diff --git a/flatpak/xdotool.json b/flatpak/xdotool.json deleted file mode 100644 index d7f41bf0ec09..000000000000 --- a/flatpak/xdotool.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "name": "xdotool", - "buildsystem": "simple", - "build-commands": [ - "make -j4 && PREFIX=./build make install", - "cp -r ./build/* /app/" - ], - "sources": [ - { - "type": "archive", - "url": "https://github.com/jordansissel/xdotool/releases/download/v3.20211022.1/xdotool-3.20211022.1.tar.gz", - "sha256": "96f0facfde6d78eacad35b91b0f46fecd0b35e474c03e00e30da3fdd345f9ada" - } - ] -} From 00d38260e170180519a1bdd3db2d0d73ea51246a Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Mon, 7 Oct 2024 21:37:43 +0800 Subject: [PATCH 268/541] fix: web send audit note (#9582) Signed-off-by: fufesou --- flutter/lib/models/platform_model.dart | 8 ++++++++ flutter/lib/models/state_model.dart | 5 ++--- flutter/lib/web/bridge.dart | 2 +- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/flutter/lib/models/platform_model.dart b/flutter/lib/models/platform_model.dart index 6bc770ff666b..0f21587ad8df 100644 --- a/flutter/lib/models/platform_model.dart +++ b/flutter/lib/models/platform_model.dart @@ -6,3 +6,11 @@ final platformFFI = PlatformFFI.instance; final localeName = PlatformFFI.localeName; RustdeskImpl get bind => platformFFI.ffiBind; + +String ffiGetByName(String name, [String arg = '']) { + return PlatformFFI.getByName(name, arg); +} + +void ffiSetByName(String name, [String value = '']) { + PlatformFFI.setByName(name, value); +} diff --git a/flutter/lib/models/state_model.dart b/flutter/lib/models/state_model.dart index 62f92db12188..3481aa2b3e1b 100644 --- a/flutter/lib/models/state_model.dart +++ b/flutter/lib/models/state_model.dart @@ -2,7 +2,6 @@ import 'package:desktop_multi_window/desktop_multi_window.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/common.dart'; import 'package:get/get.dart'; -import 'native_model.dart' if (dart.library.html) 'web_model.dart'; import '../consts.dart'; import './platform_model.dart'; @@ -83,7 +82,7 @@ class StateGlobal { } procFullscreenWeb() { - final isFullscreen = PlatformFFI.getByName('fullscreen') == 'Y'; + final isFullscreen = ffiGetByName('fullscreen') == 'Y'; String fullscreenValue = ''; if (isFullscreen && _fullscreen.isFalse) { fullscreenValue = 'N'; @@ -91,7 +90,7 @@ class StateGlobal { fullscreenValue = 'Y'; } if (fullscreenValue.isNotEmpty) { - PlatformFFI.setByName('fullscreen', fullscreenValue); + ffiSetByName('fullscreen', fullscreenValue); } } diff --git a/flutter/lib/web/bridge.dart b/flutter/lib/web/bridge.dart index 5f699114ae05..6e02328b49c0 100644 --- a/flutter/lib/web/bridge.dart +++ b/flutter/lib/web/bridge.dart @@ -1188,7 +1188,7 @@ class RustdeskImpl { Future sessionSendNote( {required UuidValue sessionId, required String note, dynamic hint}) { - throw UnimplementedError(); + return Future(() => js.context.callMethod('setByName', ['send_note', note])); } Future sessionAlternativeCodecs( From 28b6bc186fe3eaa153b25e714b31c26643557206 Mon Sep 17 00:00:00 2001 From: XLion Date: Mon, 7 Oct 2024 21:46:25 +0800 Subject: [PATCH 269/541] Add `Section` to Debian control (#9584) --- build.py | 1 + 1 file changed, 1 insertion(+) diff --git a/build.py b/build.py index 3ad206ab1898..201766b3e2a3 100755 --- a/build.py +++ b/build.py @@ -283,6 +283,7 @@ def generate_control_file(version): system2('/bin/rm -rf %s' % control_file_path) content = """Package: rustdesk +Section: net Version: %s Architecture: %s Maintainer: rustdesk From 5555ba6b2fead83092e3485b7d71b448efc09b51 Mon Sep 17 00:00:00 2001 From: RustDesk <71636191+rustdesk@users.noreply.github.com> Date: Mon, 7 Oct 2024 21:50:01 +0800 Subject: [PATCH 270/541] Revert "manifest updates from the Flathub's PR (#9581)" (#9585) This reverts commit e06f456bbd3241c20993fa4603a0c8e6ca6c9bdb. --- .github/workflows/flutter-build.yml | 4 +- flatpak/com.rustdesk.RustDesk.metainfo.xml | 40 ----------------- flatpak/rustdesk.json | 50 +++++++++------------- flatpak/xdotool.json | 15 +++++++ 4 files changed, 37 insertions(+), 72 deletions(-) delete mode 100644 flatpak/com.rustdesk.RustDesk.metainfo.xml create mode 100644 flatpak/xdotool.json diff --git a/.github/workflows/flutter-build.yml b/.github/workflows/flutter-build.yml index 989aee65f92a..dd21515ba547 100644 --- a/.github/workflows/flutter-build.yml +++ b/.github/workflows/flutter-build.yml @@ -1951,8 +1951,8 @@ jobs: wget # flatpak deps flatpak --user remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo - flatpak --user install -y flathub org.freedesktop.Platform/${{ matrix.job.arch }}/24.08 - flatpak --user install -y flathub org.freedesktop.Sdk/${{ matrix.job.arch }}/24.08 + flatpak --user install -y flathub org.freedesktop.Platform/${{ matrix.job.arch }}/23.08 + flatpak --user install -y flathub org.freedesktop.Sdk/${{ matrix.job.arch }}/23.08 # package pushd flatpak git clone https://github.com/flathub/shared-modules.git --depth=1 diff --git a/flatpak/com.rustdesk.RustDesk.metainfo.xml b/flatpak/com.rustdesk.RustDesk.metainfo.xml deleted file mode 100644 index 12842fb8abe0..000000000000 --- a/flatpak/com.rustdesk.RustDesk.metainfo.xml +++ /dev/null @@ -1,40 +0,0 @@ - - - com.rustdesk.RustDesk - RustDesk - com.rustdesk.RustDesk.desktop - CC0-1.0 - AGPL-3.0-only - RustDesk - An open-source remote desktop application designed for self-hosting, as an alternative to TeamViewer. - -

    - RustDesk is a full-featured open source remote control alternative for self-hosting and security with minimal configuration. -

    -
      -
    • Works on Windows, macOS, Linux, iOS, Android, Web.
    • -
    • Supports VP8 / VP9 / AV1 software codecs, and H264 / H265 hardware codecs.
    • -
    • Own your data, easily set up self-hosting solution on your infrastructure.
    • -
    • P2P connection with end-to-end encryption based on NaCl.
    • -
    • No administrative privileges or installation needed for Windows, elevate priviledge locally or from remote on demand.
    • -
    • We like to keep things simple and will strive to make simpler where possible.
    • -
    -

    - For self-hosting setup instructions please go to our home page. -

    -
    - - Utility - - - - https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png - - - https://rustdesk.com - https://github.com/rustdesk/rustdesk/issues - - - - -
    \ No newline at end of file diff --git a/flatpak/rustdesk.json b/flatpak/rustdesk.json index 366f83f5b406..6d7acb5b89ca 100644 --- a/flatpak/rustdesk.json +++ b/flatpak/rustdesk.json @@ -1,36 +1,19 @@ { "id": "com.rustdesk.RustDesk", "runtime": "org.freedesktop.Platform", - "runtime-version": "24.08", + "runtime-version": "23.08", "sdk": "org.freedesktop.Sdk", "command": "rustdesk", - "rename-desktop-file": "rustdesk.desktop", - "rename-icon": "rustdesk", - "cleanup": [ - "/include", - "/lib/pkgconfig", - "/share/gtk-doc" - ], + "icon": "share/icons/hicolor/scalable/apps/rustdesk.svg", "modules": [ "shared-modules/libappindicator/libappindicator-gtk3-12.10.json", - { - "name": "xdotool", - "no-autogen": true, - "make-install-args": [ - "PREFIX=${FLATPAK_DEST}" - ], - "sources": [ - { - "type": "archive", - "url": "https://github.com/jordansissel/xdotool/releases/download/v3.20211022.1/xdotool-3.20211022.1.tar.gz", - "sha256": "96f0facfde6d78eacad35b91b0f46fecd0b35e474c03e00e30da3fdd345f9ada" - } - ] - }, + "xdotool.json", { "name": "pam", - "buildsystem": "autotools", - "config-opts": [ "--disable-selinux" ], + "buildsystem": "simple", + "build-commands": [ + "./configure --disable-selinux --prefix=/app && make -j4 install" + ], "sources": [ { "type": "archive", @@ -43,25 +26,32 @@ "name": "rustdesk", "buildsystem": "simple", "build-commands": [ - "bsdtar -Oxf rustdesk.deb data.tar.xz | bsdtar -xf -", - "cp -r usr/* /app/", + "bsdtar -zxvf rustdesk.deb", + "tar -xvf ./data.tar.xz", + "cp -r ./usr/* /app/", "mkdir -p /app/bin && ln -s /app/lib/rustdesk/rustdesk /app/bin/rustdesk", - "install -Dm644 com.rustdesk.RustDesk.metainfo.xml /app/share/metainfo/com.rustdesk.RustDesk.metainfo.xml" + "mv /app/share/applications/rustdesk.desktop /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/*.desktop", + "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 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"], "sources": [ { "type": "file", - "path": "rustdesk.deb" + "path": "./rustdesk.deb" }, { "type": "file", - "path": "com.rustdesk.RustDesk.metainfo.xml" + "path": "../res/scalable.svg" } ] } ], "finish-args": [ "--share=ipc", + "--socket=x11", "--socket=fallback-x11", "--socket=wayland", "--share=network", @@ -70,4 +60,4 @@ "--socket=pulseaudio", "--talk-name=org.freedesktop.Flatpak" ] -} \ No newline at end of file +} diff --git a/flatpak/xdotool.json b/flatpak/xdotool.json new file mode 100644 index 000000000000..d7f41bf0ec09 --- /dev/null +++ b/flatpak/xdotool.json @@ -0,0 +1,15 @@ +{ + "name": "xdotool", + "buildsystem": "simple", + "build-commands": [ + "make -j4 && PREFIX=./build make install", + "cp -r ./build/* /app/" + ], + "sources": [ + { + "type": "archive", + "url": "https://github.com/jordansissel/xdotool/releases/download/v3.20211022.1/xdotool-3.20211022.1.tar.gz", + "sha256": "96f0facfde6d78eacad35b91b0f46fecd0b35e474c03e00e30da3fdd345f9ada" + } + ] +} From 9bcd0d1b03d7205d29e99f1988479c2b19940389 Mon Sep 17 00:00:00 2001 From: XLion Date: Mon, 7 Oct 2024 22:08:17 +0800 Subject: [PATCH 271/541] Add `Priority:` to Debian control (#9586) --- build.py | 1 + 1 file changed, 1 insertion(+) diff --git a/build.py b/build.py index 201766b3e2a3..b7ea0a1ef68d 100755 --- a/build.py +++ b/build.py @@ -284,6 +284,7 @@ def generate_control_file(version): content = """Package: rustdesk Section: net +Priority: optional Version: %s Architecture: %s Maintainer: rustdesk From 2591d4f04418a936768146bbea62eb81bb353760 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Tue, 8 Oct 2024 15:23:00 +0800 Subject: [PATCH 272/541] fix: web chat (#9588) * fix: web chat Signed-off-by: fufesou * add missing svg Signed-off-by: fufesou --------- Signed-off-by: fufesou --- flutter/assets/message_24dp_5F6368.svg | 1 + .../lib/desktop/widgets/remote_toolbar.dart | 59 ++++++++++++------- flutter/lib/models/chat_model.dart | 7 ++- flutter/lib/web/bridge.dart | 6 +- 4 files changed, 46 insertions(+), 27 deletions(-) create mode 100644 flutter/assets/message_24dp_5F6368.svg diff --git a/flutter/assets/message_24dp_5F6368.svg b/flutter/assets/message_24dp_5F6368.svg new file mode 100644 index 000000000000..5347a3d2d652 --- /dev/null +++ b/flutter/assets/message_24dp_5F6368.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/flutter/lib/desktop/widgets/remote_toolbar.dart b/flutter/lib/desktop/widgets/remote_toolbar.dart index e7b4d4d761fd..d92636301776 100644 --- a/flutter/lib/desktop/widgets/remote_toolbar.dart +++ b/flutter/lib/desktop/widgets/remote_toolbar.dart @@ -479,8 +479,8 @@ class _RemoteToolbarState extends State { setFullscreen: _setFullscreen, )); toolbarItems.add(_KeyboardMenu(id: widget.id, ffi: widget.ffi)); + toolbarItems.add(_ChatMenu(id: widget.id, ffi: widget.ffi)); if (!isWeb) { - toolbarItems.add(_ChatMenu(id: widget.id, ffi: widget.ffi)); toolbarItems.add(_VoiceCallMenu(id: widget.id, ffi: widget.ffi)); } if (!isWeb) toolbarItems.add(_RecordMenu()); @@ -1781,34 +1781,49 @@ class _ChatMenuState extends State<_ChatMenu> { @override Widget build(BuildContext context) { - return _IconSubmenuButton( - tooltip: 'Chat', - key: chatButtonKey, - svg: 'assets/chat.svg', - ffi: widget.ffi, - color: _ToolbarTheme.blueColor, - hoverColor: _ToolbarTheme.hoverBlueColor, - menuChildrenGetter: () => [textChat(), voiceCall()]); + if (isWeb) { + return buildTextChatButton(); + } else { + return _IconSubmenuButton( + tooltip: 'Chat', + key: chatButtonKey, + svg: 'assets/chat.svg', + ffi: widget.ffi, + color: _ToolbarTheme.blueColor, + hoverColor: _ToolbarTheme.hoverBlueColor, + menuChildrenGetter: () => [textChat(), voiceCall()]); + } + } + + buildTextChatButton() { + return _IconMenuButton( + assetName: 'assets/message_24dp_5F6368.svg', + tooltip: 'Text chat', + key: chatButtonKey, + onPressed: _textChatOnPressed, + color: _ToolbarTheme.blueColor, + hoverColor: _ToolbarTheme.hoverBlueColor, + ); } textChat() { return MenuButton( child: Text(translate('Text chat')), ffi: widget.ffi, - onPressed: () { - RenderBox? renderBox = - chatButtonKey.currentContext?.findRenderObject() as RenderBox?; - - Offset? initPos; - if (renderBox != null) { - final pos = renderBox.localToGlobal(Offset.zero); - initPos = Offset(pos.dx, pos.dy + _ToolbarTheme.dividerHeight); - } + onPressed: _textChatOnPressed); + } - widget.ffi.chatModel.changeCurrentKey( - MessageKey(widget.ffi.id, ChatModel.clientModeID)); - widget.ffi.chatModel.toggleChatOverlay(chatInitPos: initPos); - }); + _textChatOnPressed() { + RenderBox? renderBox = + chatButtonKey.currentContext?.findRenderObject() as RenderBox?; + Offset? initPos; + if (renderBox != null) { + final pos = renderBox.localToGlobal(Offset.zero); + initPos = Offset(pos.dx, pos.dy + _ToolbarTheme.dividerHeight); + } + widget.ffi.chatModel + .changeCurrentKey(MessageKey(widget.ffi.id, ChatModel.clientModeID)); + widget.ffi.chatModel.toggleChatOverlay(chatInitPos: initPos); } voiceCall() { diff --git a/flutter/lib/models/chat_model.dart b/flutter/lib/models/chat_model.dart index 778c94357042..d79c9f070222 100644 --- a/flutter/lib/models/chat_model.dart +++ b/flutter/lib/models/chat_model.dart @@ -235,13 +235,14 @@ class ChatModel with ChangeNotifier { } } - _isChatOverlayHide() => ((!isDesktop && chatIconOverlayEntry == null) || - chatWindowOverlayEntry == null); + _isChatOverlayHide() => + ((!(isDesktop || isWebDesktop) && chatIconOverlayEntry == null) || + chatWindowOverlayEntry == null); toggleChatOverlay({Offset? chatInitPos}) { if (_isChatOverlayHide()) { gFFI.invokeMethod("enable_soft_keyboard", true); - if (!isDesktop) { + if (!(isDesktop || isWebDesktop)) { showChatIconOverlay(); } showChatWindowOverlay(chatInitPos: chatInitPos); diff --git a/flutter/lib/web/bridge.dart b/flutter/lib/web/bridge.dart index 6e02328b49c0..321fdcd5a4fc 100644 --- a/flutter/lib/web/bridge.dart +++ b/flutter/lib/web/bridge.dart @@ -468,7 +468,8 @@ class RustdeskImpl { Future sessionSendChat( {required UuidValue sessionId, required String text, dynamic hint}) { - throw UnimplementedError(); + return Future( + () => js.context.callMethod('setByName', ['send_chat', text])); } Future sessionPeerOption( @@ -1188,7 +1189,8 @@ class RustdeskImpl { Future sessionSendNote( {required UuidValue sessionId, required String note, dynamic hint}) { - return Future(() => js.context.callMethod('setByName', ['send_note', note])); + return Future( + () => js.context.callMethod('setByName', ['send_note', note])); } Future sessionAlternativeCodecs( From 507de628c914a451f6a96916f5c971e5c215e8d4 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Tue, 8 Oct 2024 18:35:25 +0800 Subject: [PATCH 273/541] fix: web, 2fa, trust (#9594) Signed-off-by: fufesou --- flutter/lib/web/bridge.dart | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/flutter/lib/web/bridge.dart b/flutter/lib/web/bridge.dart index 321fdcd5a4fc..2c8d6b4b0339 100644 --- a/flutter/lib/web/bridge.dart +++ b/flutter/lib/web/bridge.dart @@ -152,7 +152,10 @@ class RustdeskImpl { required String code, required bool trustThisDevice, dynamic hint}) { - return Future(() => js.context.callMethod('setByName', ['send_2fa', code])); + return Future(() => js.context.callMethod('setByName', [ + 'send_2fa', + jsonEncode({'code': code, 'trust_this_device': trustThisDevice}) + ])); } Future sessionClose({required UuidValue sessionId, dynamic hint}) { @@ -1682,7 +1685,8 @@ class RustdeskImpl { bool sessionGetEnableTrustedDevices( {required UuidValue sessionId, dynamic hint}) { - throw UnimplementedError(); + return js.context.callMethod('getByName', ['enable_trusted_devices']) == + 'Y'; } Future mainGetTrustedDevices({dynamic hint}) { From e6d4067f489f6b4bf6b8cf6747a8cac6f251e57f Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Tue, 8 Oct 2024 21:16:07 +0800 Subject: [PATCH 274/541] refact: remote toolbar style (#9597) Signed-off-by: fufesou --- .../lib/desktop/widgets/remote_toolbar.dart | 45 +++++++++++++------ 1 file changed, 32 insertions(+), 13 deletions(-) diff --git a/flutter/lib/desktop/widgets/remote_toolbar.dart b/flutter/lib/desktop/widgets/remote_toolbar.dart index d92636301776..3d8ca5e13149 100644 --- a/flutter/lib/desktop/widgets/remote_toolbar.dart +++ b/flutter/lib/desktop/widgets/remote_toolbar.dart @@ -2336,15 +2336,33 @@ class _DraggableShowHideState extends State<_DraggableShowHide> { ); final isFullscreen = stateGlobal.fullscreen; const double iconSize = 20; + + buttonWrapper(VoidCallback? onPressed, Widget child, + {Color hoverColor = _ToolbarTheme.blueColor}) { + final bgColor = buttonStyle.backgroundColor?.resolve({}); + return TextButton( + onPressed: onPressed, + child: child, + style: buttonStyle.copyWith( + backgroundColor: MaterialStateProperty.resolveWith((states) { + if (states.contains(MaterialState.hovered)) { + return (bgColor ?? hoverColor).withOpacity(0.15); + } + return bgColor; + }), + ), + ); + } + final child = Row( mainAxisSize: MainAxisSize.min, children: [ _buildDraggable(context), - Obx(() => TextButton( - onPressed: () { + Obx(() => buttonWrapper( + () { widget.setFullscreen(!isFullscreen.value); }, - child: Tooltip( + Tooltip( message: translate( isFullscreen.isTrue ? 'Exit Fullscreen' : 'Fullscreen'), child: Icon( @@ -2358,9 +2376,9 @@ class _DraggableShowHideState extends State<_DraggableShowHide> { if (!isMacOS && !isWebDesktop) Obx(() => Offstage( offstage: isFullscreen.isFalse, - child: TextButton( - onPressed: () => widget.setMinimize(), - child: Tooltip( + child: buttonWrapper( + widget.setMinimize, + Tooltip( message: translate('Minimize'), child: Icon( Icons.remove, @@ -2369,11 +2387,11 @@ class _DraggableShowHideState extends State<_DraggableShowHide> { ), ), )), - TextButton( - onPressed: () => setState(() { + buttonWrapper( + () => setState(() { widget.toolbarState.switchShow(widget.sessionId); }), - child: Obx((() => Tooltip( + Obx((() => Tooltip( message: translate(show.isTrue ? 'Hide Toolbar' : 'Show Toolbar'), child: Icon( @@ -2387,9 +2405,9 @@ class _DraggableShowHideState extends State<_DraggableShowHide> { if (show.isTrue) { return Offstage(); } else { - return TextButton( - onPressed: () => closeConnection(id: widget.id), - child: Tooltip( + return buttonWrapper( + () => closeConnection(id: widget.id), + Tooltip( message: translate('Close'), child: Icon( Icons.close, @@ -2397,7 +2415,8 @@ class _DraggableShowHideState extends State<_DraggableShowHide> { color: _ToolbarTheme.redColor, ), ), - ); + hoverColor: _ToolbarTheme.redColor, + ).paddingOnly(left: iconSize / 2); } }) ], From 4b3b31147e2d9cbdf709ce21baaa40e18f2117ba Mon Sep 17 00:00:00 2001 From: James Date: Tue, 8 Oct 2024 23:01:54 -0300 Subject: [PATCH 275/541] Update eo.rs (#9600) --- src/lang/eo.rs | 240 ++++++++++++++++++++++++------------------------- 1 file changed, 120 insertions(+), 120 deletions(-) diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 3a406e558710..ba369e4ec110 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -6,11 +6,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("desk_tip", "Via aparato povas esti alirita kun tiu identigilo kaj pasvorto"), ("Password", "Pasvorto"), ("Ready", "Preta"), - ("Established", ""), + ("Established", "Establis"), ("connecting_status", "Konektante al la reto RustDesk..."), ("Enable service", "Ebligi servon"), ("Start service", "Starti servon"), - ("Service is running", ""), + ("Service is running", "La servo funkcias"), ("Service is not running", "La servo ne funkcias"), ("not_ready_status", "Ne preta, bonvolu kontroli la retkonekto"), ("Control Remote Desktop", "Kontroli foran aparaton"), @@ -29,33 +29,33 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable TCP tunneling", "Ebligi tunelado TCP"), ("IP Whitelisting", "Listo de IP akceptataj"), ("ID/Relay Server", "Identigila/Relajsa servilo"), - ("Import server config", "Enporti servilan agordon"), - ("Export Server Config", ""), + ("Import server config", "Importi servilan agordon"), + ("Export Server Config", "Eksporti servilan agordon"), ("Import server configuration successfully", "Importi servilan agordon sukcese"), - ("Export server configuration successfully", ""), + ("Export server configuration successfully", "Eksporti servilan agordon sukcese"), ("Invalid server configuration", "Nevalida servila agordo"), ("Clipboard is empty", "La poŝo estas malplena"), ("Stop service", "Haltu servon"), ("Change ID", "Ŝanĝi identigilon"), - ("Your new ID", ""), - ("length %min% to %max%", ""), - ("starts with a letter", ""), - ("allowed characters", ""), + ("Your new ID", "Via nova identigilo"), + ("length %min% to %max%", "longeco %min% al %max%"), + ("starts with a letter", "komencas kun letero"), + ("allowed characters", "permesitaj signoj"), ("id_change_tip", "Nur la signoj a-z, A-Z, 0-9, _ (substreko) povas esti uzataj. La unua litero povas esti inter a-z, A-Z. La longeco devas esti inter 6 kaj 16."), ("Website", "Retejo"), ("About", "Pri"), - ("Slogan_tip", ""), - ("Privacy Statement", ""), + ("Slogan_tip", "Farita kun koro en ĉi tiu ĥaosa mondo!"), + ("Privacy Statement", "Deklaro Pri Privateco"), ("Mute", "Muta"), - ("Build Date", ""), - ("Version", ""), - ("Home", ""), - ("Audio Input", "Aŭdia enigo"), - ("Enhancements", ""), - ("Hardware Codec", ""), - ("Adaptive bitrate", ""), + ("Build Date", "konstruada dato"), + ("Version", "Versio"), + ("Home", "Hejmo"), + ("Audio Input", "Aŭdia Enigo"), + ("Enhancements", "Plibonigoj"), + ("Hardware Codec", "Aparataro Kodeko"), + ("Adaptive bitrate", "Adapta bitrapido"), ("ID Server", "Servilo de identigiloj"), - ("Relay Server", "Relajsa servilo"), + ("Relay Server", "Relajsa Servilo"), ("API Server", "Servilo de API"), ("invalid_http", "Devas komenci kun http:// aŭ https://"), ("Invalid IP", "IP nevalida"), @@ -83,35 +83,35 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Successful", "Sukceso"), ("Connected, waiting for image...", "Konektita, atendante bildon..."), ("Name", "Nomo"), - ("Type", ""), + ("Type", "Tipo"), ("Modified", "Modifita"), ("Size", "Grandeco"), ("Show Hidden Files", "Montri kaŝitajn dosierojn"), ("Receive", "Akcepti"), ("Send", "Sendi"), - ("Refresh File", ""), - ("Local", ""), - ("Remote", ""), + ("Refresh File", "Aktualigu Dosieron"), + ("Local", "Loka"), + ("Remote", "Fora"), ("Remote Computer", "Fora komputilo"), ("Local Computer", "Loka komputilo"), ("Confirm Delete", "Konfermi la forigo"), - ("Delete", ""), - ("Properties", ""), - ("Multi Select", ""), - ("Select All", ""), - ("Unselect All", ""), - ("Empty Directory", ""), - ("Not an empty directory", ""), - ("Are you sure you want to delete this file?", "Ĉu vi vere volas forigi tiun dosieron?"), - ("Are you sure you want to delete this empty directory?", ""), - ("Are you sure you want to delete the file of this directory?", ""), + ("Delete", "Forigi"), + ("Properties", "Propraĵoj"), + ("Multi Select", "Pluropa Elekto"), + ("Select All", "Elektu Ĉiujn"), + ("Unselect All", "Malelektu Ĉiujn"), + ("Empty Directory", "Malplena Dosierujo"), + ("Not an empty directory", "Ne Malplena Dosierujo"), + ("Are you sure you want to delete this file?", "Ĉu vi certas, ke vi volas forigi ĉi tiun dosieron?"), + ("Are you sure you want to delete this empty directory?", "Ĉu vi certas, ke vi volas forigi ĉi tiun malplenan dosierujon?"), + ("Are you sure you want to delete the file of this directory?", "Ĉu vi certa. ke vi volas forigi la dosieron de ĉi tiu dosierujo"), ("Do this for all conflicts", "Same por ĉiuj konfliktoj"), - ("This is irreversible!", ""), + ("This is irreversible!", "Ĉi tio estas neinversigebla!"), ("Deleting", "Forigado"), ("files", "dosiero"), ("Waiting", "Atendante..."), ("Finished", "Finita"), - ("Speed", ""), + ("Speed", "Rapideco"), ("Custom Image Quality", "Agordi bildan kvaliton"), ("Privacy mode", "Modo privata"), ("Block user input", "Bloki uzanta enigo"), @@ -127,7 +127,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Optimize reaction time", "Optimigi reakcia tempo"), ("Custom", ""), ("Show remote cursor", "Montri foran kursoron"), - ("Show quality monitor", ""), + ("Show quality monitor", "Montri kvalito monitoron"), ("Disable clipboard", "Malebligi poŝon"), ("Lock after session end", "Ŝlosi foran komputilon post malkonektado"), ("Insert", "Enmeti"), @@ -170,8 +170,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Action", "Ago"), ("Add", "Aldoni"), ("Local Port", "Loka pordo"), - ("Local Address", ""), - ("Change Local Port", ""), + ("Local Address", "Loka Adreso"), + ("Change Local Port", "Ŝanĝi Loka Pordo"), ("setup_server_tip", "Se vi bezonas pli rapida konekcio, vi povas krei vian propran servilon"), ("Too short, at least 6 characters.", "Tro mallonga, almenaŭ 6 signoj."), ("The confirmation is not identical.", "Ambaŭ enigoj ne kongruas"), @@ -203,23 +203,23 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Reboot required", "Restarto deviga"), ("Unsupported display server", "La aktuala bilda servilo ne estas subtenita"), ("x11 expected", "Bonvolu uzi x11"), - ("Port", ""), + ("Port", "Pordo"), ("Settings", "Agordoj"), ("Username", " Uzanta nomo"), ("Invalid port", "Pordo nevalida"), ("Closed manually by the peer", "Manuale fermita de la samtavolano"), ("Enable remote configuration modification", "Permesi foran redaktadon de la konfiguracio"), ("Run without install", "Plenumi sen instali"), - ("Connect via relay", ""), + ("Connect via relay", "Konekti per relajso"), ("Always connect via relay", "Ĉiam konekti per relajso"), ("whitelist_tip", "Nur la IP en la blanka listo povas kontroli mian komputilon"), - ("Login", "Konekti"), - ("Verify", ""), - ("Remember me", ""), - ("Trust this device", ""), - ("Verification code", ""), - ("verification_tip", ""), - ("Logout", "Malkonekti"), + ("Login", "Ensaluti"), + ("Verify", "Kontrolis"), + ("Remember me", "Memori min"), + ("Trust this device", "Fidu ĉi tiun aparaton"), + ("Verification code", "Konfirmkodo"), + ("verification_tip", "Konfirmkodo estis sendita al la registrita retpoŝta adreso, enigu la konfirmkodon por daŭrigi ensaluti."), + ("Logout", "Elsaluti"), ("Tags", "Etikedi"), ("Search ID", "Serĉi ID"), ("whitelist_sep", "Vi povas uzi komon, punktokomon, spacon aŭ linsalton kiel apartigilo"), @@ -241,86 +241,86 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Socks5 Proxy", "Socks5 prokura servilo"), ("Socks5/Http(s) Proxy", "Socks5/Http(s) prokura servilo"), ("Discovered", "Malkovritaj"), - ("install_daemon_tip", ""), + ("install_daemon_tip", "Por komenci ĉe ekŝargo, oni devas instali sisteman servon."), ("Remote ID", "Fora identigilo"), ("Paste", "Alglui"), - ("Paste here?", ""), + ("Paste here?", "Alglui ĉi tie?"), ("Are you sure to close the connection?", "Ĉu vi vere volas fermi la konekton?"), ("Download new version", "Elŝuti la novan version"), ("Touch mode", "Tuŝa modo"), - ("Mouse mode", ""), - ("One-Finger Tap", ""), - ("Left Mouse", ""), - ("One-Long Tap", ""), - ("Two-Finger Tap", ""), - ("Right Mouse", ""), - ("One-Finger Move", ""), - ("Double Tap & Move", ""), - ("Mouse Drag", ""), - ("Three-Finger vertically", ""), - ("Mouse Wheel", ""), - ("Two-Finger Move", ""), - ("Canvas Move", ""), - ("Pinch to Zoom", ""), - ("Canvas Zoom", ""), + ("Mouse mode", "musa modo"), + ("One-Finger Tap", "Unufingra Frapeto"), + ("Left Mouse", "Maldekstra Muso"), + ("One-Long Tap", "Unulonga Frapeto"), + ("Two-Finger Tap", "Dufingra Frapeto"), + ("Right Mouse", "Deskra Muso"), + ("One-Finger Move", "Unufingra Movo"), + ("Double Tap & Move", "Duobla Frapeto & Movo"), + ("Mouse Drag", "Muso Trenadi"), + ("Three-Finger vertically", "Tri Figroj Vertikale"), + ("Mouse Wheel", "Musa Rado"), + ("Two-Finger Move", "Dufingra Movo"), + ("Canvas Move", "Kanvasa Movo"), + ("Pinch to Zoom", "Pinĉi al Zomo"), + ("Canvas Zoom", "Kanvasa Zomo"), ("Reset canvas", "Restarigi kanvaso"), ("No permission of file transfer", "Neniu permeso de dosiertransigo"), ("Note", "Notu"), - ("Connection", ""), - ("Share Screen", ""), - ("Chat", ""), - ("Total", ""), - ("items", ""), - ("Selected", ""), - ("Screen Capture", ""), - ("Input Control", ""), - ("Audio Capture", ""), - ("File Connection", ""), - ("Screen Connection", ""), - ("Do you accept?", ""), - ("Open System Setting", ""), - ("How to get Android input permission?", ""), - ("android_input_permission_tip1", ""), - ("android_input_permission_tip2", ""), - ("android_new_connection_tip", ""), - ("android_service_will_start_tip", ""), - ("android_stop_service_tip", ""), - ("android_version_audio_tip", ""), - ("android_start_service_tip", ""), - ("android_permission_may_not_change_tip", ""), - ("Account", ""), - ("Overwrite", ""), - ("This file exists, skip or overwrite this file?", ""), - ("Quit", ""), - ("Help", ""), - ("Failed", ""), - ("Succeeded", ""), - ("Someone turns on privacy mode, exit", ""), - ("Unsupported", ""), - ("Peer denied", ""), - ("Please install plugins", ""), - ("Peer exit", ""), - ("Failed to turn off", ""), - ("Turned off", ""), - ("Language", ""), - ("Keep RustDesk background service", ""), - ("Ignore Battery Optimizations", ""), - ("android_open_battery_optimizations_tip", ""), - ("Start on boot", ""), - ("Start the screen sharing service on boot, requires special permissions", ""), - ("Connection not allowed", ""), + ("Connection", "Konekto"), + ("Share Screen", "Kunhavigi Ekranon"), + ("Chat", "Babilo"), + ("Total", "Sumo"), + ("items", "eroj"), + ("Selected", "Elektita"), + ("Screen Capture", "Ekrankapto"), + ("Input Control", "Eniga Kontrolo"), + ("Audio Capture", "Sonkontrolo"), + ("File Connection", "Dosiero Konekto"), + ("Screen Connection", "Ekrono konekto"), + ("Do you accept?", "Ĉu vi akceptas?"), + ("Open System Setting", "Malfermi Sistemajn Agordojn"), + ("How to get Android input permission?", "Kiel akiri Android enigajn permesojn"), + ("android_input_permission_tip1", "Por ke fora aparato regu vian Android-aparaton per muso aŭ tuŝo, vi devas permesi al RustDesk uzi la servon \"Alirebleco\"."), + ("android_input_permission_tip2", "Bonvolu iri al la sekva paĝo de sistemaj agordoj, trovi kaj eniri [Instatajn Servojn], ŝalti la servon [RustDesk Enigo]."), + ("android_new_connection_tip", "Nova kontrolpeto estis ricevita, kiu volas kontroli vian nunan aparaton."), + ("android_service_will_start_tip", "Ŝalti \"Ekrankapto\" aŭtomate startos la servon, permesante al aliaj aparatoj peti konekton al via aparato."), + ("android_stop_service_tip", "Fermante la servon aŭtomate fermos ĉiujn establitajn konektojn."), + ("android_version_audio_tip", "La nuna versio da Android ne subtenas sonkapton, bonvolu ĝisdatigi al Android 10 aŭ pli alta."), + ("android_start_service_tip", "Frapu [Komenci servo] aŭ ebligu la permeson de [Ekrankapto] por komenci la servon de kundivido de ekrano."), + ("android_permission_may_not_change_tip", "Permesoj por establitaj konektoj neble estas ŝanĝitaj tuj ĝis rekonektitaj."), + ("Account", "Konto"), + ("Overwrite", "anstataŭigi"), + ("This file exists, skip or overwrite this file?", "Ĉi tiu dosiero ekzistas, ĉu preterlasi aŭ anstataŭi ĉi tiun dosieron?"), + ("Quit", "Forlasi"), + ("Help", "Helpi"), + ("Failed", "Malsukcesa"), + ("Succeeded", "Sukcesa"), + ("Someone turns on privacy mode, exit", "Iu ŝaltas modon privata, Eliro"), + ("Unsupported", "Nesubtenata"), + ("Peer denied", "Samulo rifuzita"), + ("Please install plugins", "Bonvolu instali kromprogramojn"), + ("Peer exit", "Samulo eliras"), + ("Failed to turn off", "Malsukcesis malŝalti"), + ("Turned off", "Malŝaltita"), + ("Language", "Lingvo"), + ("Keep RustDesk background service", "Tenu RustDesk fonan servon"), + ("Ignore Battery Optimizations", "Ignoru Bateria Optimumigojn"), + ("android_open_battery_optimizations_tip", "Se vi volas malŝalti ĉi tiun funkcion, bonvolu iri al la sekva paĝo de agordoj de la aplikaĵo de RustDesk, trovi kaj eniri [Baterio], Malmarku [Senrestrikta]"), + ("Start on boot", "Komencu ĉe ekfunkciigo"), + ("Start the screen sharing service on boot, requires special permissions", "Komencu la servon de kundivido de ekrano ĉe lanĉo, postulas specialajn permesojn"), + ("Connection not allowed", "Konekto ne rajtas"), ("Legacy mode", ""), - ("Map mode", ""), - ("Translate mode", ""), - ("Use permanent password", ""), - ("Use both passwords", ""), - ("Set permanent password", ""), - ("Enable remote restart", ""), - ("Restart remote device", ""), - ("Are you sure you want to restart", ""), - ("Restarting remote device", ""), - ("remote_restarting_tip", ""), - ("Copied", ""), + ("Map mode", "Mapa modo"), + ("Translate mode", "Traduki modo"), + ("Use permanent password", "Uzu permanenta pasvorto"), + ("Use both passwords", "Uzu ambaŭ pasvorto"), + ("Set permanent password", "Starigi permanenta pasvorto"), + ("Enable remote restart", "Permesi fora restartas"), + ("Restart remote device", "Restartu fora aparato"), + ("Are you sure you want to restart", "Ĉu vi certas, ke vi volas restarti"), + ("Restarting remote device", "Restartas fora aparato"), + ("remote_restarting_tip", "Fora aparato restartiĝas, bonvolu fermi ĉi tiun mesaĝkeston kaj rekonekti kun permanenta pasvorto post iom da tempo"), + ("Copied", "Kopiita"), ("Exit Fullscreen", "Eliru Plenekranon"), ("Fullscreen", "Plenekrane"), ("Mobile Actions", "Poŝtelefonaj Agoj"), @@ -330,8 +330,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Ratio", "Proporcio"), ("Image Quality", "Bilda Kvalito"), ("Scroll Style", "Ruluma Stilo"), - ("Show Toolbar", ""), - ("Hide Toolbar", ""), + ("Show Toolbar", "Montri Ilobreton"), + ("Hide Toolbar", "Kaŝi Ilobreton"), ("Direct Connection", "Rekta Konekto"), ("Relay Connection", "Relajsa Konekto"), ("Secure Connection", "Sekura Konekto"), From 38fcf4e039767bd8b00b1060fdfa76c0ad9cbde8 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Wed, 9 Oct 2024 10:19:24 +0800 Subject: [PATCH 276/541] typo --- src/lang/cn.rs | 2 +- src/lang/tw.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lang/cn.rs b/src/lang/cn.rs index db942f598f05..7cc616e14270 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -310,7 +310,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Start the screen sharing service on boot, requires special permissions", "开机自动启动屏幕共享服务,此功能需要一些特殊权限。"), ("Connection not allowed", "对方不允许连接"), ("Legacy mode", "传统模式"), - ("Map mode", "1:1 传输"), + ("Map mode", "1:1 传输"), ("Translate mode", "翻译模式"), ("Use permanent password", "使用固定密码"), ("Use both passwords", "同时使用两种密码"), diff --git a/src/lang/tw.rs b/src/lang/tw.rs index c935b5552ffd..5bb26acce158 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -310,7 +310,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Start the screen sharing service on boot, requires special permissions", "開機時啟動螢幕分享服務,需要特殊權限。"), ("Connection not allowed", "不允許連線"), ("Legacy mode", "傳統模式"), - ("Map mode", "1:1 傳輸模式"), + ("Map mode", "1:1 傳輸模式"), ("Translate mode", "翻譯模式"), ("Use permanent password", "使用固定密碼"), ("Use both passwords", "同時使用兩種密碼"), From 59d7bf1e860adb4cf8bc287ba4db1726ed19e49c Mon Sep 17 00:00:00 2001 From: Ibnul Mutaki <36250619+cacing69@users.noreply.github.com> Date: Wed, 9 Oct 2024 09:44:20 +0700 Subject: [PATCH 277/541] Update indonesian translate (#9601) * update id trans * update id trans * update inv * update ind trans --- src/lang/id.rs | 74 +++++++++++++++++++++++++------------------------- 1 file changed, 37 insertions(+), 37 deletions(-) diff --git a/src/lang/id.rs b/src/lang/id.rs index c16243aedd2f..c0920dfcb573 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -2,8 +2,8 @@ lazy_static::lazy_static! { pub static ref T: std::collections::HashMap<&'static str, &'static str> = [ ("Status", "Status"), - ("Your Desktop", "Desktop Anda"), - ("desk_tip", "Desktop Anda dapat diakses dengan ID dan kata sandi ini."), + ("Your Desktop", "Kamu"), + ("desk_tip", "Desktop kamu dapat diakses dengan ID dan kata sandi ini."), ("Password", "Kata sandi"), ("Ready", "Sudah siap"), ("Established", "Didirikan"), @@ -12,7 +12,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Start service", "Mulai Layanan"), ("Service is running", "Layanan berjalan"), ("Service is not running", "Layanan tidak berjalan"), - ("not_ready_status", "Belum siap. Silakan periksa koneksi Anda"), + ("not_ready_status", "Belum siap. Silakan periksa koneksi"), ("Control Remote Desktop", "Kontrol Remote Desktop"), ("Transfer file", "File Transfer"), ("Connect", "Hubungkan"), @@ -22,7 +22,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("TCP tunneling", "Tunneling TCP"), ("Remove", "Hapus"), ("Refresh random password", "Perbarui kata sandi acak"), - ("Set your own password", "Tetapkan kata sandi Anda"), + ("Set your own password", "Tetapkan kata sandi"), ("Enable keyboard/mouse", "Aktifkan Keyboard/Mouse"), ("Enable clipboard", "Aktifkan Papan Klip"), ("Enable file transfer", "Aktifkan Transfer file"), @@ -37,7 +37,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is empty", "Papan klip kosong"), ("Stop service", "Hentikan Layanan"), ("Change ID", "Ubah ID"), - ("Your new ID", "ID baru anda"), + ("Your new ID", "ID baru"), ("length %min% to %max%", "panjang %min% s/d %max%"), ("starts with a letter", "Dimulai dengan huruf"), ("allowed characters", "Karakter yang dapat digunakan"), @@ -69,10 +69,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Retry", "Coba lagi"), ("OK", "Oke"), ("Password Required", "Kata sandi tidak boleh kosong"), - ("Please enter your password", "Silahkan masukkan kata sandi anda"), + ("Please enter your password", "Silahkan masukkan kata sandi"), ("Remember password", "Ingat kata sandi"), ("Wrong Password", "Kata sandi Salah"), - ("Do you want to enter again?", "Apakah anda ingin masuk lagi?"), + ("Do you want to enter again?", "Apakah kamu ingin mencoba lagi?"), ("Connection Error", "Kesalahan koneksi"), ("Error", "Kesalahan"), ("Reset by the peer", "Direset oleh rekan"), @@ -102,9 +102,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Unselect All", "Batalkan Pilihan Semua"), ("Empty Directory", "Folder Kosong"), ("Not an empty directory", "Folder tidak kosong"), - ("Are you sure you want to delete this file?", "Apakah anda yakin untuk menghapus file ini?"), - ("Are you sure you want to delete this empty directory?", "Apakah anda yakin untuk menghapus folder ini?"), - ("Are you sure you want to delete the file of this directory?", "Apakah anda yakin untuk menghapus file dan folder ini?"), + ("Are you sure you want to delete this file?", "Apakah kamu yakin untuk menghapus file ini?"), + ("Are you sure you want to delete this empty directory?", "Apakah yakin yakin untuk menghapus folder ini?"), + ("Are you sure you want to delete the file of this directory?", "Apakah yakin yakin untuk menghapus file dan folder ini?"), ("Do this for all conflicts", "Lakukan untuk semua konflik"), ("This is irreversible!", "Ini tidak dapat diubah!"), ("Deleting", "Menghapus"), @@ -150,20 +150,20 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Click to download", "Klik untuk unduh"), ("Click to update", "Klik untuk memperbarui"), ("Configure", "Konfigurasi"), - ("config_acc", "Untuk mengontrol Desktop Anda dari jarak jauh, Anda perlu memberikan izin \"Aksesibilitas\" RustDesk."), - ("config_screen", "Untuk mengakses Desktop Anda dari jarak jauh, Anda perlu memberikan izin \"Perekaman Layar\" RustDesk."), + ("config_acc", "Agar bisa mengontrol Desktopmu dari jarak jauh, Kamu harus memberikan izin \"Aksesibilitas\" untuk RustDesk."), + ("config_screen", "Agar bisa mengakses Desktopmu dari jarak jauh, kamu harus memberikan izin \"Perekaman Layar\" untuk RustDesk."), ("Installing ...", "Menginstall"), ("Install", "Instal"), ("Installation", "Instalasi"), ("Installation Path", "Direktori Instalasi"), ("Create start menu shortcuts", "Buat pintasan start menu"), ("Create desktop icon", "Buat icon desktop"), - ("agreement_tip", "Dengan memulai instalasi, Anda menerima perjanjian lisensi."), + ("agreement_tip", "Dengan memulai proses instalasi, Kamu menerima perjanjian lisensi."), ("Accept and Install", "Terima dan Install"), ("End-user license agreement", "Perjanjian lisensi pengguna"), ("Generating ...", "Memproses..."), - ("Your installation is lower version.", "Instalasi Anda adalah versi yang lebih rendah."), - ("not_close_tcp_tip", "Jangan tutup jendela ini saat menggunakan tunnel"), + ("Your installation is lower version.", "Kamu menggunakan versi instalasi yang lebih rendah."), + ("not_close_tcp_tip", "Pastikan jendela ini tetap terbuka saat menggunakan tunnel."), ("Listening ...", "Menghubungkan..."), ("Remote Host", "Host Remote"), ("Remote Port", "Port Remote"), @@ -186,10 +186,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Direct and unencrypted connection", "Koneksi langsung dan tanpa enkripsi"), ("Relayed and unencrypted connection", "Koneksi relay dan tanpa enkripsi"), ("Enter Remote ID", "Masukkan ID Remote"), - ("Enter your password", "Masukkan kata sandi anda"), + ("Enter your password", "Masukkan kata sandi"), ("Logging in...", "Masuk..."), ("Enable RDP session sharing", "Aktifkan berbagi sesi RDP"), - ("Auto Login", "Login Otomatis (Hanya berlaku jika Anda mengatur \"Kunci setelah sesi berakhir\")"), + ("Auto Login", "Login Otomatis (Hanya berlaku jika sudah mengatur \"Kunci setelah sesi berakhir\")"), ("Enable direct IP access", "Aktifkan Akses IP Langsung"), ("Rename", "Ubah nama"), ("Space", "Spasi"), @@ -241,11 +241,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Socks5 Proxy", "Proksi Socks5"), ("Socks5/Http(s) Proxy", "Proksi Socks5/Http(s)"), ("Discovered", "Telah ditemukan"), - ("install_daemon_tip", "Untuk memulai saat boot, Anda perlu menginstal system service."), + ("install_daemon_tip", "Untuk dapat berjalan saat sistem menyala, kamu perlu menginstal layanan sistem (system service/daemon)."), ("Remote ID", "ID Remote"), ("Paste", "Tempel"), ("Paste here?", "Tempel disini?"), - ("Are you sure to close the connection?", "Apakah anda yakin akan menutup koneksi?"), + ("Are you sure to close the connection?", "Apakah kamu yakin akan menutup koneksi?"), ("Download new version", "Unduh versi baru"), ("Touch mode", "Mode Layar Sentuh"), ("Mouse mode", "Mode Mouse"), @@ -373,7 +373,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Write a message", "Tulis pesan"), ("Prompt", ""), ("Please wait for confirmation of UAC...", "Harap tunggu konfirmasi UAC"), - ("elevated_foreground_window_tip", "Jendela remote desktop ini memerlukan hak akses khusus, jadi anda tidak bisa menggunakan mouse dan keyboard untuk sementara. Anda bisa meminta pihak pengguna yang diremote untuk menyembunyikan jendela ini atau klik tombol elevasi di jendela pengaturan koneksi. Untuk menghindari masalah ini, direkomendasikan untuk menginstall aplikasi secara permanen"), + ("elevated_foreground_window_tip", "Jendela yang sedang aktif di remote desktop memerlukan hak istimewa yang lebih tinggi untuk beroperasi, sehingga mouse dan keyboard tidak dapat digunakan sementara waktu. Kamu bisa meminta pengguna jarak jauh untuk meminimalkan jendela saat ini, atau klik tombol elevasi di jendela manajemen koneksi. Untuk menghindari masalah ini, disarankan untuk menginstal software di perangkat remote secara permanen."), ("Disconnected", "Terputus"), ("Other", "Lainnya"), ("Confirm before closing multiple tabs", "Konfirmasi sebelum menutup banyak tab"), @@ -586,29 +586,29 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("powered_by_me", "Didukung oleh RustDesk"), ("outgoing_only_desk_tip", "Ini adalah edisi yang sudah kustomisasi.\nAnda dapat terhubung ke perangkat lain, tetapi perangkat lain tidak dapat terhubung ke perangkat Anda."), ("preset_password_warning", "Edisi yang dikustomisasi ini dilengkapi dengan kata sandi bawaan. Siapa pun yang mengetahui kata sandi ini dapat memperoleh kontrol penuh atas perangkat Anda. Jika Anda tidak mengharapkan ini, segera hapus pemasangan aplikasi tersebut."), - ("Security Alert", ""), - ("My address book", ""), - ("Personal", ""), - ("Owner", ""), - ("Set shared password", ""), - ("Exist in", ""), + ("Security Alert", "Peringatan Keamanan"), + ("My address book", "Daftar Kontak"), + ("Personal", "Personal"), + ("Owner", "Pemilik"), + ("Set shared password", "Atus kata sandi kolaboratif"), + ("Exist in", "Ada di"), ("Read-only", ""), ("Read/Write", ""), ("Full Control", ""), - ("share_warning_tip", ""), + ("share_warning_tip", "Informasi di atas bersifat publik dan dapat dilihat oleh orang lain."), ("Everyone", ""), - ("ab_web_console_tip", ""), - ("allow-only-conn-window-open-tip", ""), - ("no_need_privacy_mode_no_physical_displays_tip", ""), - ("Follow remote cursor", ""), - ("Follow remote window focus", ""), - ("default_proxy_tip", ""), - ("no_audio_input_device_tip", ""), + ("ab_web_console_tip", "Detail Lain di Konsol Web"), + ("allow-only-conn-window-open-tip", "Koneksi hanya diperbolehkan jika jendela RustDesk sedang terbuka."), + ("no_need_privacy_mode_no_physical_displays_tip", "Karena tidak ada layar fisik, mode privasi tidak perlu diaktifkan."), + ("Follow remote cursor", "Ikuti kursor yang terhubung"), + ("Follow remote window focus", "Ikuti jendela remote yang sedang aktif"), + ("default_proxy_tip", "Pengaturan standar untuk protokol dan port adalah Socks5 dan 1080."), + ("no_audio_input_device_tip", "Perangkat input audio tidak terdeteksi."), ("Incoming", ""), ("Outgoing", ""), - ("Clear Wayland screen selection", ""), - ("clear_Wayland_screen_selection_tip", ""), - ("confirm_clear_Wayland_screen_selection_tip", ""), + ("Clear Wayland screen selection", "Kosongkan pilihan layar Wayland"), + ("clear_Wayland_screen_selection_tip", "Setelah mengosongkan pilihan layar, Anda bisa memilih kembali layar untuk dibagi"), + ("confirm_clear_Wayland_screen_selection_tip", "Kamu yakin ingin membersihkan pemilihan layar Wayland?"), ("android_new_voice_call_tip", ""), ("texture_render_tip", ""), ("Use texture rendering", ""), From 227f154ee7a8ca6704345d7888b223fb29cae451 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jernej=20Simon=C4=8Di=C4=8D?= <1800143+jernejs@users.noreply.github.com> Date: Thu, 10 Oct 2024 01:35:46 +0200 Subject: [PATCH 278/541] Add missing Slovenian translations (#9606) --- src/lang/sl.rs | 530 ++++++++++++++++++++++++------------------------- 1 file changed, 265 insertions(+), 265 deletions(-) diff --git a/src/lang/sl.rs b/src/lang/sl.rs index bba9499d75ad..7d18ef829823 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -37,19 +37,19 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is empty", "Odložišče je prazno"), ("Stop service", "Ustavi storitev"), ("Change ID", "Spremeni ID"), - ("Your new ID", ""), - ("length %min% to %max%", ""), - ("starts with a letter", ""), - ("allowed characters", ""), + ("Your new ID", "Vaš nov ID"), + ("length %min% to %max%", "dolžina od %min% do %max%"), + ("starts with a letter", "začne se s črko"), + ("allowed characters", "dovoljeni znaki"), ("id_change_tip", "Dovoljeni znaki so a-z, A-Z (brez šumnikov), 0-9 in _. Prvi znak mora biti črka, dolžina od 6 do 16 znakov."), ("Website", "Spletna stran"), ("About", "O programu"), ("Slogan_tip", ""), - ("Privacy Statement", ""), + ("Privacy Statement", "Izjava o zasebnosti"), ("Mute", "Izklopi zvok"), - ("Build Date", ""), - ("Version", ""), - ("Home", ""), + ("Build Date", "Datum graditve"), + ("Version", "Različica"), + ("Home", "Začetek"), ("Audio Input", "Avdio vhod"), ("Enhancements", "Izboljšave"), ("Hardware Codec", "Strojni kodek"), @@ -210,27 +210,27 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "Povezavo ročno prekinil odjemalec"), ("Enable remote configuration modification", "Omogoči oddaljeno spreminjanje nastavitev"), ("Run without install", "Zaženi brez namestitve"), - ("Connect via relay", ""), + ("Connect via relay", "Poveži preko posrednika"), ("Always connect via relay", "Vedno poveži preko posrednika"), ("whitelist_tip", "Dostop je možen samo iz dovoljenih IPjev"), ("Login", "Prijavi"), - ("Verify", ""), - ("Remember me", ""), - ("Trust this device", ""), - ("Verification code", ""), - ("verification_tip", ""), + ("Verify", "Preveri"), + ("Remember me", "Zapomni si me"), + ("Trust this device", "Zaupaj tej napravi"), + ("Verification code", "Koda za preverjanje"), + ("verification_tip", "Kodo za preverjanje prejmete na registrirani e-poštni naslov"), ("Logout", "Odjavi"), ("Tags", "Oznake"), ("Search ID", "Išči ID"), ("whitelist_sep", "Naslovi ločeni z vejico, podpičjem, presledkom ali novo vrstico"), ("Add ID", "Dodaj ID"), ("Add Tag", "Dodaj oznako"), - ("Unselect all tags", ""), + ("Unselect all tags", "Odznači vse oznake"), ("Network error", "Omrežna napaka"), ("Username missed", "Up. ime izpuščeno"), ("Password missed", "Geslo izpuščeno"), ("Wrong credentials", "Napačne poverilnice"), - ("The verification code is incorrect or has expired", ""), + ("The verification code is incorrect or has expired", "Koda za preverjanje je napačna, ali pa je potekla"), ("Edit Tag", "Uredi oznako"), ("Forget Password", "Pozabi geslo"), ("Favorites", "Priljubljene"), @@ -248,7 +248,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Are you sure to close the connection?", "Ali želite prekiniti povezavo?"), ("Download new version", "Prenesi novo različico"), ("Touch mode", "Način dotika"), - ("Mouse mode", "Način mišle"), + ("Mouse mode", "Način miške"), ("One-Finger Tap", "Tap z enim prstom"), ("Left Mouse", "Leva tipka miške"), ("One-Long Tap", "Dolg tap z enim prstom"), @@ -286,8 +286,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("android_service_will_start_tip", "Z vklopom zajema zaslona se bo samodejno zagnala storitev, ki omogoča da oddaljene naprave pošljejo zahtevo za povezavo na vašo napravo."), ("android_stop_service_tip", "Z zaustavitvijo storitve bodo samodejno prekinjene vse oddaljene povezave."), ("android_version_audio_tip", "Trenutna različica Androida ne omogoča zajema zvoka. Za zajem zvoka nadgradite na Android 10 ali novejši."), - ("android_start_service_tip", ""), - ("android_permission_may_not_change_tip", ""), + ("android_start_service_tip", "Tapnite [Zaženi storitev] ali pa omogočite pravico [Zajemanje zaslona] za zagon storitve deljenja zaslona."), + ("android_permission_may_not_change_tip", "Pravic za že vzpostavljene povezave ne morete spremeniti brez ponovne vzpostavitve povezave."), ("Account", "Račun"), ("Overwrite", "Prepiši"), ("This file exists, skip or overwrite this file?", "Datoteka obstaja, izpusti ali prepiši?"), @@ -306,8 +306,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keep RustDesk background service", "Ohrani RustDeskovo storitev v ozadju"), ("Ignore Battery Optimizations", "Prezri optimizacije baterije"), ("android_open_battery_optimizations_tip", "Če želite izklopiti to možnost, pojdite v nastavitve aplikacije RustDesk, poiščite »Baterija« in izklopite »Neomejeno«"), - ("Start on boot", ""), - ("Start the screen sharing service on boot, requires special permissions", ""), + ("Start on boot", "Zaženi ob vklopu"), + ("Start the screen sharing service on boot, requires special permissions", "Zaženi storitev deljenja zaslona ob vklopu, zahteva posebna dovoljenja"), ("Connection not allowed", "Povezava ni dovoljena"), ("Legacy mode", "Stari način"), ("Map mode", "Način preslikave"), @@ -330,8 +330,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Ratio", "Razmerje"), ("Image Quality", "Kakovost slike"), ("Scroll Style", "Način drsenja"), - ("Show Toolbar", ""), - ("Hide Toolbar", ""), + ("Show Toolbar", "Prikaži orodno vrstico"), + ("Hide Toolbar", "Skrij orodno vrstico"), ("Direct Connection", "Neposredna povezava"), ("Relay Connection", "Posredovana povezava"), ("Secure Connection", "Zavarovana povezava"), @@ -342,7 +342,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Security", "Varnost"), ("Theme", "Tema"), ("Dark Theme", "Temna tema"), - ("Light Theme", ""), + ("Light Theme", "Svetla tema"), ("Dark", "Temna"), ("Light", "Svetla"), ("Follow System", "Sistemska"), @@ -359,8 +359,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Audio Input Device", "Vhodna naprava za zvok"), ("Use IP Whitelisting", "Omogoči seznam dovoljenih IP naslovov"), ("Network", "Mreža"), - ("Pin Toolbar", ""), - ("Unpin Toolbar", ""), + ("Pin Toolbar", "Pripni orodno vrstico"), + ("Unpin Toolbar", "Odpni orodno vrstico"), ("Recording", "Snemanje"), ("Directory", "Imenik"), ("Automatically record incoming sessions", "Samodejno snemaj vhodne seje"), @@ -409,244 +409,244 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by web console", "Ročno zaprto iz spletne konzole"), ("Local keyboard type", "Lokalna vrsta tipkovnice"), ("Select local keyboard type", "Izberite lokalno vrsto tipkovnice"), - ("software_render_tip", ""), - ("Always use software rendering", ""), - ("config_input", ""), - ("config_microphone", ""), - ("request_elevation_tip", ""), - ("Wait", ""), - ("Elevation Error", ""), - ("Ask the remote user for authentication", ""), - ("Choose this if the remote account is administrator", ""), - ("Transmit the username and password of administrator", ""), - ("still_click_uac_tip", ""), - ("Request Elevation", ""), - ("wait_accept_uac_tip", ""), - ("Elevate successfully", ""), - ("uppercase", ""), - ("lowercase", ""), - ("digit", ""), - ("special character", ""), - ("length>=8", ""), - ("Weak", ""), - ("Medium", ""), - ("Strong", ""), - ("Switch Sides", ""), - ("Please confirm if you want to share your desktop?", ""), - ("Display", ""), - ("Default View Style", ""), - ("Default Scroll Style", ""), - ("Default Image Quality", ""), - ("Default Codec", ""), - ("Bitrate", ""), - ("FPS", ""), - ("Auto", ""), - ("Other Default Options", ""), - ("Voice call", ""), - ("Text chat", ""), - ("Stop voice call", ""), - ("relay_hint_tip", ""), - ("Reconnect", ""), - ("Codec", ""), - ("Resolution", ""), - ("No transfers in progress", ""), - ("Set one-time password length", ""), - ("RDP Settings", ""), - ("Sort by", ""), - ("New Connection", ""), - ("Restore", ""), - ("Minimize", ""), - ("Maximize", ""), - ("Your Device", ""), - ("empty_recent_tip", ""), - ("empty_favorite_tip", ""), - ("empty_lan_tip", ""), - ("empty_address_book_tip", ""), - ("eg: admin", ""), - ("Empty Username", ""), - ("Empty Password", ""), - ("Me", ""), - ("identical_file_tip", ""), - ("show_monitors_tip", ""), - ("View Mode", ""), - ("login_linux_tip", ""), - ("verify_rustdesk_password_tip", ""), - ("remember_account_tip", ""), - ("os_account_desk_tip", ""), - ("OS Account", ""), - ("another_user_login_title_tip", ""), - ("another_user_login_text_tip", ""), - ("xorg_not_found_title_tip", ""), - ("xorg_not_found_text_tip", ""), - ("no_desktop_title_tip", ""), - ("no_desktop_text_tip", ""), - ("No need to elevate", ""), - ("System Sound", ""), - ("Default", ""), - ("New RDP", ""), - ("Fingerprint", ""), - ("Copy Fingerprint", ""), - ("no fingerprints", ""), - ("Select a peer", ""), - ("Select peers", ""), - ("Plugins", ""), - ("Uninstall", ""), - ("Update", ""), - ("Enable", ""), - ("Disable", ""), - ("Options", ""), - ("resolution_original_tip", ""), - ("resolution_fit_local_tip", ""), - ("resolution_custom_tip", ""), - ("Collapse toolbar", ""), - ("Accept and Elevate", ""), - ("accept_and_elevate_btn_tooltip", ""), - ("clipboard_wait_response_timeout_tip", ""), - ("Incoming connection", ""), - ("Outgoing connection", ""), - ("Exit", ""), - ("Open", ""), - ("logout_tip", ""), - ("Service", ""), - ("Start", ""), - ("Stop", ""), - ("exceed_max_devices", ""), - ("Sync with recent sessions", ""), - ("Sort tags", ""), - ("Open connection in new tab", ""), - ("Move tab to new window", ""), - ("Can not be empty", ""), - ("Already exists", ""), - ("Change Password", ""), - ("Refresh Password", ""), - ("ID", ""), - ("Grid View", ""), - ("List View", ""), - ("Select", ""), - ("Toggle Tags", ""), - ("pull_ab_failed_tip", ""), - ("push_ab_failed_tip", ""), - ("synced_peer_readded_tip", ""), - ("Change Color", ""), - ("Primary Color", ""), - ("HSV Color", ""), - ("Installation Successful!", ""), - ("Installation failed!", ""), - ("Reverse mouse wheel", ""), - ("{} sessions", ""), - ("scam_title", ""), - ("scam_text1", ""), - ("scam_text2", ""), - ("Don't show again", ""), - ("I Agree", ""), - ("Decline", ""), - ("Timeout in minutes", ""), - ("auto_disconnect_option_tip", ""), - ("Connection failed due to inactivity", ""), - ("Check for software update on startup", ""), - ("upgrade_rustdesk_server_pro_to_{}_tip", ""), - ("pull_group_failed_tip", ""), - ("Filter by intersection", ""), - ("Remove wallpaper during incoming sessions", ""), - ("Test", ""), - ("display_is_plugged_out_msg", ""), - ("No displays", ""), - ("Open in new window", ""), - ("Show displays as individual windows", ""), - ("Use all my displays for the remote session", ""), - ("selinux_tip", ""), - ("Change view", ""), - ("Big tiles", ""), - ("Small tiles", ""), - ("List", ""), - ("Virtual display", ""), - ("Plug out all", ""), - ("True color (4:4:4)", ""), - ("Enable blocking user input", ""), - ("id_input_tip", ""), - ("privacy_mode_impl_mag_tip", ""), - ("privacy_mode_impl_virtual_display_tip", ""), - ("Enter privacy mode", ""), - ("Exit privacy mode", ""), - ("idd_not_support_under_win10_2004_tip", ""), - ("input_source_1_tip", ""), - ("input_source_2_tip", ""), - ("Swap control-command key", ""), - ("swap-left-right-mouse", ""), - ("2FA code", ""), - ("More", ""), - ("enable-2fa-title", ""), - ("enable-2fa-desc", ""), - ("wrong-2fa-code", ""), - ("enter-2fa-title", ""), - ("Email verification code must be 6 characters.", ""), - ("2FA code must be 6 digits.", ""), - ("Multiple Windows sessions found", ""), - ("Please select the session you want to connect to", ""), - ("powered_by_me", ""), - ("outgoing_only_desk_tip", ""), - ("preset_password_warning", ""), - ("Security Alert", ""), - ("My address book", ""), - ("Personal", ""), - ("Owner", ""), - ("Set shared password", ""), - ("Exist in", ""), - ("Read-only", ""), - ("Read/Write", ""), - ("Full Control", ""), - ("share_warning_tip", ""), - ("Everyone", ""), - ("ab_web_console_tip", ""), - ("allow-only-conn-window-open-tip", ""), - ("no_need_privacy_mode_no_physical_displays_tip", ""), - ("Follow remote cursor", ""), - ("Follow remote window focus", ""), - ("default_proxy_tip", ""), - ("no_audio_input_device_tip", ""), - ("Incoming", ""), - ("Outgoing", ""), - ("Clear Wayland screen selection", ""), - ("clear_Wayland_screen_selection_tip", ""), - ("confirm_clear_Wayland_screen_selection_tip", ""), - ("android_new_voice_call_tip", ""), - ("texture_render_tip", ""), - ("Use texture rendering", ""), - ("Floating window", ""), - ("floating_window_tip", ""), - ("Keep screen on", ""), - ("Never", ""), - ("During controlled", ""), - ("During service is on", ""), - ("Capture screen using DirectX", ""), - ("Back", ""), - ("Apps", ""), - ("Volume up", ""), - ("Volume down", ""), - ("Power", ""), - ("Telegram bot", ""), - ("enable-bot-tip", ""), - ("enable-bot-desc", ""), - ("cancel-2fa-confirm-tip", ""), - ("cancel-bot-confirm-tip", ""), - ("About RustDesk", ""), - ("Send clipboard keystrokes", ""), - ("network_error_tip", ""), - ("Unlock with PIN", ""), - ("Requires at least {} characters", ""), - ("Wrong PIN", ""), - ("Set PIN", ""), - ("Enable trusted devices", ""), - ("Manage trusted devices", ""), - ("Platform", ""), - ("Days remaining", ""), - ("enable-trusted-devices-tip", ""), - ("Parent directory", ""), - ("Resume", ""), - ("Invalid file name", ""), - ("one-way-file-transfer-tip", ""), - ("Authentication Required", ""), - ("Authenticate", ""), - ("web_id_input_tip", ""), + ("software_render_tip", "Če na Linuxu uporabljate Nvidino grafično kartico in se oddaljeno okno zapre takoj po vzpostavitvi povezave, lahko pomaga preklop na odprtokodni gonilnik Nouveau in uporaba programskega upodabljanja. Potreben je ponovni zagon programa."), + ("Always use software rendering", "Vedno uporabi programsko upodabljanje"), + ("config_input", "Za nadzor oddaljenega namizja s tipkovnico, rabi RustDesk pravico »Nadzor vnosa«."), + ("config_microphone", "Za zajem zvoka, rabi RustDesk pravico »Snemanje zvoka«."), + ("request_elevation_tip", "Lahko tudi zaprosite za dvig pravic, če je kdo na oddaljeni strani."), + ("Wait", "Čakaj"), + ("Elevation Error", "Napaka pri povzdigovanju"), + ("Ask the remote user for authentication", "Vprašaj oddaljenega uporabnika za prijavo"), + ("Choose this if the remote account is administrator", "Izberite to, če ima oddaljeni uporabnik skrbniške pravice"), + ("Transmit the username and password of administrator", "Vnesite poverilnice za skrbnika"), + ("still_click_uac_tip", "Oddaljeni uporabnik mora klikniti »Da« v oknu za nadzor uporabniškega računa."), + ("Request Elevation", "Zahtevaj povzdig pravic"), + ("wait_accept_uac_tip", "Počakajte na potrditev oddaljenega uporabnika v oknu za nadzor uporabniškega računa."), + ("Elevate successfully", "Povzdig pravic uspešen"), + ("uppercase", "velike črke"), + ("lowercase", "male črke"), + ("digit", "številke"), + ("special character", "posebni znaki"), + ("length>=8", "dolžina>=8"), + ("Weak", "Šibko"), + ("Medium", "Srednje"), + ("Strong", "Močno"), + ("Switch Sides", "Zamenjaj strani"), + ("Please confirm if you want to share your desktop?", "Potrdite, če želite deliti vaše namizje"), + ("Display", "Zaslon"), + ("Default View Style", "Privzeti način prikaza"), + ("Default Scroll Style", "Privzeti način drsenja"), + ("Default Image Quality", "Privzeta kakovost slike"), + ("Default Codec", "Privzeti kodek"), + ("Bitrate", "Bitna hitrost"), + ("FPS", "Sličice/sekundo"), + ("Auto", "Samodejno"), + ("Other Default Options", "Ostale privzete možnosti"), + ("Voice call", "Glasovni klic"), + ("Text chat", "Besedilni klepet"), + ("Stop voice call", "Prekini glasovni klic"), + ("relay_hint_tip", "Morda neposredna povezava ni možna; lahko se poikusite povezati preko posrednika. Če želite uporabiti posrednika ob prvem poizkusu vzpotavljanja povezave, lahko na konec IDja dodate »/r«, ali pa izberete možnost »Vedno poveži preko posrednika« v kartici nedavnih sej, če le-ta obstja."), + ("Reconnect", "Ponovna povezava"), + ("Codec", "Kodek"), + ("Resolution", "Ločljivost"), + ("No transfers in progress", "Trenutno ni prenosov"), + ("Set one-time password length", "Nastavi dolžino enkratnega gesla"), + ("RDP Settings", "Nastavitve za RDP"), + ("Sort by", "Razvrsti po"), + ("New Connection", "Nova povezava"), + ("Restore", "Obnovi"), + ("Minimize", "Minimiziraj"), + ("Maximize", "Maksimiziraj"), + ("Your Device", "Vaša naprava"), + ("empty_recent_tip", "Oops, ni nedavnih sej.\nPripravite novo."), + ("empty_favorite_tip", "Nimate še priljubljenih partnerjev?\nVzpostavite povezavo, in jo dodajte med priljubljene."), + ("empty_lan_tip", "Nismo našli še nobenih partnerjev."), + ("empty_address_book_tip", "Vaš adresar je prazen."), + ("eg: admin", "npr. admin"), + ("Empty Username", "Prazno uporabniško ime"), + ("Empty Password", "Prazno geslo"), + ("Me", "Jaz"), + ("identical_file_tip", "Datoteka je enaka partnerjevi"), + ("show_monitors_tip", "Prikaži monitorje v orodni vrstici"), + ("View Mode", "Način prikazovanja"), + ("login_linux_tip", "Prijaviti se morate v oddaljeni Linux račun in omogočiti namizno sejo X."), + ("verify_rustdesk_password_tip", "Preveri geslo za RustDesk"), + ("remember_account_tip", "Zapomni si ta račun"), + ("os_account_desk_tip", "Ta račun se uporabi za prijavo v oddaljeni sistem in omogči namizno sejo v napravi brez monitorja."), + ("OS Account", "Račun operacijskega sistema"), + ("another_user_login_title_tip", "Prijavljen je že drug uporabnik"), + ("another_user_login_text_tip", "Prekini"), + ("xorg_not_found_title_tip", "Xorg ni najden"), + ("xorg_not_found_text_tip", "Namestite Xorg"), + ("no_desktop_title_tip", "Namizno okolje ni na voljo"), + ("no_desktop_text_tip", "Namestite GNOME"), + ("No need to elevate", "Povzdig pravic ni potreben"), + ("System Sound", "Sistemski zvok"), + ("Default", "Privzeto"), + ("New RDP", "Nova RDP povezava"), + ("Fingerprint", "Prstni odtis"), + ("Copy Fingerprint", "Kopiraj prstni odtis"), + ("no fingerprints", "ni prstnega odtisa"), + ("Select a peer", "Izberite partnerja"), + ("Select peers", "Izberite partnerje"), + ("Plugins", "Vključki"), + ("Uninstall", "Odstrani"), + ("Update", "Posodobi"), + ("Enable", "Omogoči"), + ("Disable", "Onemogoči"), + ("Options", "Možnosti"), + ("resolution_original_tip", "Izvirna ločljivost"), + ("resolution_fit_local_tip", "Prilagodi lokalni ločljivosti"), + ("resolution_custom_tip", "Ločljivost po meri"), + ("Collapse toolbar", "Strni orodno vrstico"), + ("Accept and Elevate", "Sprejmi in povzdigni pravice"), + ("accept_and_elevate_btn_tooltip", "Sprejmi povezavo in preko nadzora uporabniškera računa povišaj pravice"), + ("clipboard_wait_response_timeout_tip", "Časovna omejitev pri kopiranju je potekla"), + ("Incoming connection", "Dohodna povezava"), + ("Outgoing connection", "Odhodna povezava"), + ("Exit", "Izhod"), + ("Open", "Odpri"), + ("logout_tip", "Ali ste prepričani, da se želite odjaviti?"), + ("Service", "Storitev"), + ("Start", "Zaženi"), + ("Stop", "Ustavi"), + ("exceed_max_devices", "Dosegli ste največje dovoljeno število upravljanih naprav."), + ("Sync with recent sessions", "Sinhroniziraj z nedavnimi sejami"), + ("Sort tags", "Uredi oznake"), + ("Open connection in new tab", "Odpri povezavo na novem zavihku"), + ("Move tab to new window", "Premakni zavihek v novo okno"), + ("Can not be empty", "Ne more biti prazno"), + ("Already exists", "Že obstaja"), + ("Change Password", "Spremeni geslo"), + ("Refresh Password", "Osveži geslo"), + ("ID", "ID"), + ("Grid View", "Mrežni pogled"), + ("List View", "Pogled seznama"), + ("Select", "Izberi"), + ("Toggle Tags", "Preklopi oznake"), + ("pull_ab_failed_tip", "Adresarja ni bilo mogoče osvežiti"), + ("push_ab_failed_tip", "Adresarja ni bilo mogoče poslati na strežnik"), + ("synced_peer_readded_tip", "Naprave, ki so bile prisotne v nedavnih sejah bodo sinhronizirane z adresarjem."), + ("Change Color", "Spremeni barvo"), + ("Primary Color", "Osnovne barve"), + ("HSV Color", "Barve HSV"), + ("Installation Successful!", "Namestitev uspešna"), + ("Installation failed!", "Namestitev ni uspela"), + ("Reverse mouse wheel", "Obrni smer drsenja miškinega kolesca"), + ("{} sessions", "{} sej"), + ("scam_title", "Lahko gre za prevaro!"), + ("scam_text1", "V primeru, da vas je nekdo, ki ga ne poznate in mu zaupate prosil, da uporabite RustDesk, prekinite klic in program zaprite."), + ("scam_text2", "RustDesk omogoča popoln nadzor nad vašim računalnikom in telefonom, in se lahko uporabi za krajo vašega denarja ali pa zasebnih podatkov."), + ("Don't show again", "Ne prikaži znova"), + ("I Agree", "Strinjam se"), + ("Decline", "Zavrni"), + ("Timeout in minutes", "Časovna omejitev v minutah"), + ("auto_disconnect_option_tip", "Samodejno prekini neaktivne seje"), + ("Connection failed due to inactivity", "Povezava je bila prekinjena zaradi neaktivnosti"), + ("Check for software update on startup", "Preveri za posodobitve ob zagonu"), + ("upgrade_rustdesk_server_pro_to_{}_tip", "Prosimo, nadgradite RustDesk Server Pro na različico {} ali novejšo."), + ("pull_group_failed_tip", "Osveževanje skupine ni uspelo"), + ("Filter by intersection", "Filtriraj po preseku"), + ("Remove wallpaper during incoming sessions", "Odstrani sliko ozadja ob dohodnih povezavah"), + ("Test", "Test"), + ("display_is_plugged_out_msg", "Zaslon je bil odklopljen, preklop na primarni zaslon."), + ("No displays", "Ni zaslonov"), + ("Open in new window", "Odpri v novem oknu"), + ("Show displays as individual windows", "Prikaži zaslone kot ločena okna"), + ("Use all my displays for the remote session", "Uporabi vse zaslone za oddaljeno sejo"), + ("selinux_tip", "Na vaši napravi je omogčen SELinux, kar lahko povzroča težave pri oddaljenem nadzoru"), + ("Change view", "Spremeni pogled"), + ("Big tiles", "Velike ploščice"), + ("Small tiles", "Majhne ploščice"), + ("List", "Seznam"), + ("Virtual display", "Navidezni zaslon"), + ("Plug out all", "Odklopi vse"), + ("True color (4:4:4)", "Popolne barve (4:4:4)"), + ("Enable blocking user input", "Omogoči blokiranje vnosa"), + ("id_input_tip", "Vnesete lahko ID, neposredni IP naslov, ali pa domeno in vrata (:)\nČe želite dostopati do naprave na drugem strežniku, pripnite naslov strežnika (@?key=), npr. 9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nČe želite dostopati do naprave na javnem strežniku, vnesite »@public«; ključ za javni strežnik ni potreben.\nČe želite vsiliti povezavo preko posrednika, pripnite »/r« na konec IDja, npr. »9123456234/r«."), + ("privacy_mode_impl_mag_tip", "Način 1"), + ("privacy_mode_impl_virtual_display_tip", "Način 2"), + ("Enter privacy mode", "Vstopi v zasebni način"), + ("Exit privacy mode", "Izstopi iz zasebnega načina"), + ("idd_not_support_under_win10_2004_tip", "Posredni gonilnik ni podprt. Za uporabo rabite Windows 10 2004 ali novejšo različico."), + ("input_source_1_tip", "Vir vnosa 1"), + ("input_source_2_tip", "Vir vnosa 2"), + ("Swap control-command key", "Zamenjaj tipki Ctrl-Command"), + ("swap-left-right-mouse", "Zamenjaj levo in desno tipko miške"), + ("2FA code", "Koda za dvostopenjsko preverjanje"), + ("More", "Več"), + ("enable-2fa-title", "Omogoči dvostopenjsko preverjanje"), + ("enable-2fa-desc", "Pripravite vaš TOTP avtentikator. Uporabite lahko programe kot so Authy, Microsoft ali Google Authenticator, na vašem telefonu ali računalniku.\n\nZa omogočanje dvostopenjskega preverjanja, skenirajte QR kodo in vnesite kodo, ki jo prikaže aplikacija."), + ("wrong-2fa-code", "Kode ni bilo mogoče preveriti. Preverite, da je koda pravilna, in da je nastavitev ure točna."), + ("enter-2fa-title", "Dvostopenjsko preverjanje"), + ("Email verification code must be 6 characters.", "E-poštna koda za preverjanje mora imeti 6 znakov."), + ("2FA code must be 6 digits.", "Koda za dvostopenjsko preverjanje mora imeti 6 znakov."), + ("Multiple Windows sessions found", "Najdenih je bilo več Windows sej"), + ("Please select the session you want to connect to", "Izberite sejo, v katero se želite povezati"), + ("powered_by_me", "Uporablja tehnologijo RustDesk"), + ("outgoing_only_desk_tip", "To je prilagojena različica.\nLahko se povežete na druge naprave, druge naprave pa se k vam ne morejo povezati."), + ("preset_password_warning", "Ta prilagojena različica ima prednastavljeno geslo. Kdorkoli, ki pozna to geslo, lahko prevzame popoln nadzor nad vašim računalnikom. Če tega niste pričakovali, takoj odstranite program."), + ("Security Alert", "Varnostno opozorilo"), + ("My address book", "Moj adresar"), + ("Personal", "Osebni"), + ("Owner", "Lastnik"), + ("Set shared password", "Nastavi deljeno geslo"), + ("Exist in", "Obstaja v"), + ("Read-only", "Samo za branje"), + ("Read/Write", "Branje/pisanje"), + ("Full Control", "Popoln nadzor"), + ("share_warning_tip", "Zgornja polja so deljena, in vidna vsem"), + ("Everyone", "Vsi"), + ("ab_web_console_tip", "Več na spletni konzoli"), + ("allow-only-conn-window-open-tip", "Dovoli povezavo samo če je okno RustDeska odprto"), + ("no_need_privacy_mode_no_physical_displays_tip", "Ni fizičnih zaslonov, zasebni način ni potreben"), + ("Follow remote cursor", "Sledi oddaljenemu kazalcu"), + ("Follow remote window focus", "Sledi oddaljenemu fokusu"), + ("default_proxy_tip", "Privzeti protokol je Socks5 na vratih 1080"), + ("no_audio_input_device_tip", "Ni bilo možno najti vhodne zvočne naprave"), + ("Incoming", "Dohodno"), + ("Outgoing", "Odhodno"), + ("Clear Wayland screen selection", "Počisti izbiro Wayland zaslona"), + ("clear_Wayland_screen_selection_tip", "Po čiščenju izbire Wayland zaslona lahko ponovno izberete zaslon za delitev"), + ("confirm_clear_Wayland_screen_selection_tip", "Ali res želite počistiti izbiro Wayland zaslona?"), + ("android_new_voice_call_tip", "Prejeli ste prošnjo za nov glasovni klic. Če sprejmete, bo zvok preklopljen na glasovno komunikacijo."), + ("texture_render_tip", "Uporabi upodabljanje tekstur, za gladkejše slike. Izklopite, če imate težave pri upodabljanju."), + ("Use texture rendering", "Uporabi upodabljanje tekstur"), + ("Floating window", "Plavajoče okno"), + ("floating_window_tip", "Pomaga pri RustDesk storitvi v ozadju"), + ("Keep screen on", "Ohranite zaslon prižgan"), + ("Never", "Nikoli"), + ("During controlled", "Med nadzorom"), + ("During service is on", "Med vklopljeno storitvijo"), + ("Capture screen using DirectX", "Uporabi DirectX za zajem zaslona"), + ("Back", "Nazaj"), + ("Apps", "Aplikacije"), + ("Volume up", "Glasneje"), + ("Volume down", "Tišje"), + ("Power", "Vklop/izklop"), + ("Telegram bot", "Telegram bot"), + ("enable-bot-tip", "Če vklopite to možnost, lahko dobite kodo za dvostopenjsko preverjanje od bota. Lahko se uporabi tudi za obveščanje o povezavi."), + ("enable-bot-desc", "1. Odprite pogovor z @BotFather.\n2. Pošljite ukaz »/newbot« in prejeli boste žeton.\n3. Začnite pogovor z na novo narejenim botom. Pošljite sporočilo z desno poševnico (/) kot npr. »/hello« za aktivacijo."), + ("cancel-2fa-confirm-tip", "Ali ste prepričani, da želite ukiniti dvostopenjsko preverjanje?"), + ("cancel-bot-confirm-tip", "Ali ste prepričani, da želite ukiniti Telegram bota?"), + ("About RustDesk", "O RustDesku"), + ("Send clipboard keystrokes", "Vtipkaj vsebino odložišča"), + ("network_error_tip", "Preverite vašo mrežno povezavo, nato kliknite Ponovi."), + ("Unlock with PIN", "Odkleni s PINom"), + ("Requires at least {} characters", "Potrebuje vsaj {} znakov."), + ("Wrong PIN", "Napačen PIN"), + ("Set PIN", "Nastavi PIN"), + ("Enable trusted devices", "Omogoči zaupanja vredne naprave"), + ("Manage trusted devices", "Upravljaj zaupanja vredne naprave"), + ("Platform", "Platforma"), + ("Days remaining", "Preostane dni"), + ("enable-trusted-devices-tip", "Na zaupanja vrednih napravah ni potrebno dvostopenjsko preverjanje"), + ("Parent directory", "Nadrejena mapa"), + ("Resume", "Nadaljuj"), + ("Invalid file name", "Neveljavno ime datoteke"), + ("one-way-file-transfer-tip", "Enosmerni prenos datotek je omogočen na nadzorovani strani"), + ("Authentication Required", "Potrebno je preverjanje pristnosti"), + ("Authenticate", "Preverjanje pristnosti"), + ("web_id_input_tip", "Vnesete lahko ID iz istega strežnika, neposredni dostop preko IP naslova v spletnem odjemalcu ni podprt.\nČe želite dostopati do naprave na drugem strežniku, pripnite naslov strežnika (@?key=), npr. 9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nČe želite dostopati do naprave na javnem strežniku, vnesite »@public«; ključ za javni strežnik ni potreben."), ].iter().cloned().collect(); } From 22c84bbbd19d85e4064355f94e48921893e909b3 Mon Sep 17 00:00:00 2001 From: Ibnul Mutaki <36250619+cacing69@users.noreply.github.com> Date: Thu, 10 Oct 2024 09:12:24 +0700 Subject: [PATCH 279/541] Update ID translation (#9609) * update id trans * update id trans * update inv * update ind trans * update ind trans * update ind trans --- src/lang/id.rs | 74 +++++++++++++++++++++++++------------------------- 1 file changed, 37 insertions(+), 37 deletions(-) diff --git a/src/lang/id.rs b/src/lang/id.rs index c0920dfcb573..b52bd7b946a0 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -2,7 +2,7 @@ lazy_static::lazy_static! { pub static ref T: std::collections::HashMap<&'static str, &'static str> = [ ("Status", "Status"), - ("Your Desktop", "Kamu"), + ("Your Desktop", "Desktop Kamu"), ("desk_tip", "Desktop kamu dapat diakses dengan ID dan kata sandi ini."), ("Password", "Kata sandi"), ("Ready", "Sudah siap"), @@ -607,46 +607,46 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Incoming", ""), ("Outgoing", ""), ("Clear Wayland screen selection", "Kosongkan pilihan layar Wayland"), - ("clear_Wayland_screen_selection_tip", "Setelah mengosongkan pilihan layar, Anda bisa memilih kembali layar untuk dibagi"), + ("clear_Wayland_screen_selection_tip", "Setelah mengosongkan pilihan layar, Kamu bisa memilih kembali layar untuk dibagi"), ("confirm_clear_Wayland_screen_selection_tip", "Kamu yakin ingin membersihkan pemilihan layar Wayland?"), - ("android_new_voice_call_tip", ""), - ("texture_render_tip", ""), - ("Use texture rendering", ""), + ("android_new_voice_call_tip", "Kamu mendapatkan permintaan panggilan suara baru. Jika diterima, audio akan berubah menjadi komunikasi suara."), + ("texture_render_tip", "Aktifkan rendering tekstur untuk membuat tampilan gambar lebih mulus. Kamu dapat menonaktifkan opsi ini jika terjadi masalah saat merender."), + ("Use texture rendering", "Aktifkan rendering tekstur"), ("Floating window", ""), - ("floating_window_tip", ""), - ("Keep screen on", ""), - ("Never", ""), - ("During controlled", ""), + ("floating_window_tip", "Untuk menjaga layanan/service RustDesk agar tetap aktif"), + ("Keep screen on", "Biarkan layar tetap menyala"), + ("Never", "Tidak pernah"), + ("During controlled", "Dalam proses pengendalian"), ("During service is on", ""), - ("Capture screen using DirectX", ""), - ("Back", ""), - ("Apps", ""), - ("Volume up", ""), - ("Volume down", ""), + ("Capture screen using DirectX", "Rekam layar dengan DirectX"), + ("Back", "Kembali"), + ("Apps", "App"), + ("Volume up", "Naikkan volume"), + ("Volume down", "Turunkan volume"), ("Power", ""), ("Telegram bot", ""), - ("enable-bot-tip", ""), - ("enable-bot-desc", ""), - ("cancel-2fa-confirm-tip", ""), - ("cancel-bot-confirm-tip", ""), - ("About RustDesk", ""), - ("Send clipboard keystrokes", ""), - ("network_error_tip", ""), - ("Unlock with PIN", ""), - ("Requires at least {} characters", ""), - ("Wrong PIN", ""), - ("Set PIN", ""), - ("Enable trusted devices", ""), - ("Manage trusted devices", ""), - ("Platform", ""), - ("Days remaining", ""), - ("enable-trusted-devices-tip", ""), - ("Parent directory", ""), - ("Resume", ""), - ("Invalid file name", ""), - ("one-way-file-transfer-tip", ""), - ("Authentication Required", ""), - ("Authenticate", ""), - ("web_id_input_tip", ""), + ("enable-bot-tip", "Jika fitur ini diaktifkan, Kamu dapat menerima kode 2FA dari bot, serta mendapatkan notifikasi tentang koneksi."), + ("enable-bot-desc", "1. Buka chat dengan @BotFather.\n2. Kirim perintah \"/newbot\". Setelah menyelesaikan langkah ini, Kamu akan mendapatkan token\n3. Mulai percakapan dengan bot yang baru dibuat. Kirim pesan yang dimulai dengan garis miring (\"/\") seperti \"/hello\" untuk mengaktifkannya."), + ("cancel-2fa-confirm-tip", "Apakah Kamu yakin ingin membatalkan 2FA?"), + ("cancel-bot-confirm-tip", "Apakah Kamu yakin ingin membatalkan bot Telegram?"), + ("About RustDesk", "Tentang RustDesk"), + ("Send clipboard keystrokes", "Kirim keystrokes clipboard"), + ("network_error_tip", "Periksa koneksi internet, lalu klik \"Coba lagi\"."), + ("Unlock with PIN", "Buka menggunakan PIN"), + ("Requires at least {} characters", "Memerlukan setidaknya {} karakter."), + ("Wrong PIN", "PIN salah"), + ("Set PIN", "Atur PIN"), + ("Enable trusted devices", "Izinkan perangkat tepercaya"), + ("Manage trusted devices", "Kelola perangkat tepercaya"), + ("Platform", "Platform"), + ("Days remaining", "Sisa hari"), + ("enable-trusted-devices-tip", "Tidak memerlukan verifikasi 2FA pada perangkat tepercaya."), + ("Parent directory", "Direktori utama"), + ("Resume", "Lanjutkan"), + ("Invalid file name", "Nama file tidak valid"), + ("one-way-file-transfer-tip", "Transfer file satu arah (One-way) telah diaktifkan pada sisi yang dikendalikan."), + ("Authentication Required", "Diperlukan autentikasi"), + ("Authenticate", "Autentikasi"), + ("web_id_input_tip", "Kamu bisa memasukkan ID pada server yang sama, akses IP langsung tidak didukung di klien web.\nJika Anda ingin mengakses perangkat di server lain, silakan tambahkan alamat server (@?key=), contohnya:\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nUntuk mengakses perangkat di server publik, cukup masukkan \"@public\", tanpa kunci/key."), ].iter().cloned().collect(); } From 97f02ed25ea3a2089fb13a04ebb864a2f1e0e1b7 Mon Sep 17 00:00:00 2001 From: 21pages Date: Fri, 11 Oct 2024 09:52:09 +0800 Subject: [PATCH 280/541] Web password source (#9618) * ensure window init finish Signed-off-by: 21pages * web password source Signed-off-by: 21pages --------- Signed-off-by: 21pages --- flutter/lib/common.dart | 2 +- flutter/lib/models/model.dart | 2 +- flutter/lib/models/web_model.dart | 6 ++++++ flutter/lib/web/bridge.dart | 22 ++++++++++++++-------- 4 files changed, 22 insertions(+), 10 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 99a24781b957..8feb2402f8a5 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -2383,7 +2383,7 @@ connect(BuildContext context, String id, ), ); } else { - if (isWebDesktop) { + if (isWeb) { Navigator.push( context, MaterialPageRoute( diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 3f2dcade9ae1..8466443ee80b 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -372,7 +372,7 @@ class FfiModel with ChangeNotifier { } else if (name == 'plugin_option') { handleOption(evt); } else if (name == "sync_peer_hash_password_to_personal_ab") { - if (desktopType == DesktopType.main) { + if (desktopType == DesktopType.main || isWeb) { final id = evt['id']; final hash = evt['hash']; if (id != null && hash != null) { diff --git a/flutter/lib/models/web_model.dart b/flutter/lib/models/web_model.dart index a5740af61a13..a4312d959c72 100644 --- a/flutter/lib/models/web_model.dart +++ b/flutter/lib/models/web_model.dart @@ -1,6 +1,7 @@ // ignore_for_file: avoid_web_libraries_in_flutter import 'dart:convert'; +import 'dart:js_interop'; import 'dart:typed_data'; import 'dart:js'; import 'dart:html'; @@ -107,6 +108,10 @@ class PlatformFFI { sessionId: sessionId, display: display, ptr: ptr); Future init(String appType) async { + Completer completer = Completer(); + context["onInitFinished"] = () { + completer.complete(); + }; context.callMethod('init'); version = getByName('version'); window.onContextMenu.listen((event) { @@ -121,6 +126,7 @@ class PlatformFFI { print('json.decode fail(): $e'); } }; + return completer.future; } void setEventCallback(void Function(Map) fun) { diff --git a/flutter/lib/web/bridge.dart b/flutter/lib/web/bridge.dart index 2c8d6b4b0339..adcbadb30e97 100644 --- a/flutter/lib/web/bridge.dart +++ b/flutter/lib/web/bridge.dart @@ -85,7 +85,11 @@ class RustdeskImpl { dynamic hint}) { return js.context.callMethod('setByName', [ 'session_add_sync', - jsonEncode({'id': id, 'password': password}) + jsonEncode({ + 'id': id, + 'password': password, + 'is_shared_password': isSharedPassword + }) ]); } @@ -1118,7 +1122,8 @@ class RustdeskImpl { } Future mainRemovePeer({required String id, dynamic hint}) { - return Future(() => js.context.callMethod('setByName', ['remove', id])); + return Future( + () => js.context.callMethod('setByName', ['remove_peer', id])); } bool mainHasHwcodec({dynamic hint}) { @@ -1146,27 +1151,28 @@ class RustdeskImpl { } Future mainSaveAb({required String json, dynamic hint}) { - throw UnimplementedError(); + return Future(() => js.context.callMethod('setByName', ['save_ab', json])); } Future mainClearAb({dynamic hint}) { - throw UnimplementedError(); + return Future(() => js.context.callMethod('setByName', ['clear_ab'])); } Future mainLoadAb({dynamic hint}) { - throw UnimplementedError(); + return Future(() => js.context.callMethod('getByName', ['load_ab'])); } Future mainSaveGroup({required String json, dynamic hint}) { - throw UnimplementedError(); + return Future( + () => js.context.callMethod('setByName', ['save_group', json])); } Future mainClearGroup({dynamic hint}) { - throw UnimplementedError(); + return Future(() => js.context.callMethod('setByName', ['clear_group'])); } Future mainLoadGroup({dynamic hint}) { - throw UnimplementedError(); + return Future(() => js.context.callMethod('getByName', ['load_group'])); } Future sessionSendPointer( From 844b853074bbdb8b9d7ec7f168a5965bb8d4d69b Mon Sep 17 00:00:00 2001 From: Ibnul Mutaki <36250619+cacing69@users.noreply.github.com> Date: Fri, 11 Oct 2024 09:43:22 +0700 Subject: [PATCH 281/541] update ID trans (#9621) --- src/lang/id.rs | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/lang/id.rs b/src/lang/id.rs index b52bd7b946a0..efedb159778c 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -2,8 +2,8 @@ lazy_static::lazy_static! { pub static ref T: std::collections::HashMap<&'static str, &'static str> = [ ("Status", "Status"), - ("Your Desktop", "Desktop Kamu"), - ("desk_tip", "Desktop kamu dapat diakses dengan ID dan kata sandi ini."), + ("Your Desktop", "Layar Utama"), + ("desk_tip", "Layar kamu dapat diakses dengan ID dan kata sandi ini."), ("Password", "Kata sandi"), ("Ready", "Sudah siap"), ("Established", "Didirikan"), @@ -12,10 +12,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Start service", "Mulai Layanan"), ("Service is running", "Layanan berjalan"), ("Service is not running", "Layanan tidak berjalan"), - ("not_ready_status", "Belum siap. Silakan periksa koneksi"), - ("Control Remote Desktop", "Kontrol Remote Desktop"), - ("Transfer file", "File Transfer"), - ("Connect", "Hubungkan"), + ("not_ready_status", "Belum siap digunakan. Silakan periksa koneksi"), + ("Control Remote Desktop", "Kontrol PC dari jarak jauh"), + ("Transfer file", "Transfer File"), + ("Connect", "Sambungkan"), ("Recent sessions", "Sesi Terkini"), ("Address book", "Buku Alamat"), ("Confirmation", "Konfirmasi"), @@ -172,14 +172,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Local Port", "Port Lokal"), ("Local Address", "Alamat lokal"), ("Change Local Port", "Ubah Port Lokal"), - ("setup_server_tip", "Untuk mendapatkan koneksi yang lebih baik, disarankan untuk menginstal di server anda sendiri"), + ("setup_server_tip", "Untuk koneksi yang lebih baik, silakan konfigurasi di server pribadi"), ("Too short, at least 6 characters.", "Terlalu pendek, setidaknya 6 karekter."), ("The confirmation is not identical.", "Konfirmasi tidak identik."), ("Permissions", "Perizinan"), ("Accept", "Terima"), ("Dismiss", "Hentikan"), ("Disconnect", "Terputus"), - ("Enable file copy and paste", "Izinkan salin dan tempel file"), + ("Enable file copy and paste", "Izinkan copy dan paste"), ("Connected", "Terhubung"), ("Direct and encrypted connection", "Koneksi langsung dan terenkripsi"), ("Relayed and encrypted connection", "Koneksi relay dan terenkripsi"), @@ -199,7 +199,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please enter the folder name", "Silahkan masukkan nama folder"), ("Fix it", "Perbaiki"), ("Warning", "Peringatan"), - ("Login screen using Wayland is not supported", "Layar masuk menggunakan Wayland tidak didukung"), + ("Login screen using Wayland is not supported", "Login screen dengan Wayland tidak didukung"), ("Reboot required", "Diperlukan boot ulang"), ("Unsupported display server", "Server tampilan tidak didukung "), ("x11 expected", "Diperlukan x11"), @@ -394,9 +394,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Accept sessions via click", "Izinkan sesi dengan klik"), ("Accept sessions via both", "Izinkan sesi dengan keduanya"), ("Please wait for the remote side to accept your session request...", "Harap tunggu pihak pengguna remote untuk menerima permintaan sesi..."), - ("One-time Password", "Kata sandi sekali pakai"), - ("Use one-time password", "Gunakan kata sandi sekali pakai"), - ("One-time password length", "Panjang kata sandi sekali pakai"), + ("One-time Password", "Kata sandi sementara"), + ("Use one-time password", "Gunakan kata sandi sementara"), + ("One-time password length", "Panjang kata sandi sementara"), ("Request access to your device", "Permintaan akses ke perangkat ini"), ("Hide connection management window", "Sembunyikan jendela pengaturan koneksi"), ("hide_cm_tip", "Izinkan untuk menyembunyikan hanya jika menerima sesi melalui kata sandi dan menggunakan kata sandi permanen"), From cde7620eda101b463c79ce7a47ef95cf4be6f3b3 Mon Sep 17 00:00:00 2001 From: 21pages Date: Fri, 11 Oct 2024 16:35:15 +0800 Subject: [PATCH 282/541] fix web peer card tap (#9622) Signed-off-by: 21pages --- flutter/lib/common/widgets/peer_card.dart | 180 +++++++++++----------- flutter/lib/main.dart | 3 +- flutter/lib/models/peer_tab_model.dart | 2 +- 3 files changed, 94 insertions(+), 91 deletions(-) diff --git a/flutter/lib/common/widgets/peer_card.dart b/flutter/lib/common/widgets/peer_card.dart index 7827298b6d07..246b337a98b7 100644 --- a/flutter/lib/common/widgets/peer_card.dart +++ b/flutter/lib/common/widgets/peer_card.dart @@ -58,27 +58,33 @@ class _PeerCardState extends State<_PeerCard> stateGlobal.isPortrait.isTrue ? _buildPortrait() : _buildLandscape()); } + Widget gestureDetector({required Widget child}) { + final PeerTabModel peerTabModel = Provider.of(context); + final peer = super.widget.peer; + return GestureDetector( + onDoubleTap: peerTabModel.multiSelectionMode || peerTabModel.isShiftDown + ? 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; - final PeerTabModel peerTabModel = Provider.of(context); return Card( margin: EdgeInsets.symmetric(horizontal: 2), - child: GestureDetector( - onTap: () { - if (peerTabModel.multiSelectionMode) { - peerTabModel.select(peer); - } else { - if (!isWebDesktop) { - connectInPeerTab(context, peer, widget.tab); - } - } - }, - onDoubleTap: isWebDesktop - ? () => connectInPeerTab(context, peer, widget.tab) - : null, - onLongPress: () { - peerTabModel.select(peer); - }, + child: gestureDetector( child: Container( padding: EdgeInsets.only(left: 12, top: 8, bottom: 8), child: _buildPeerTile(context, peer, null)), @@ -86,7 +92,6 @@ class _PeerCardState extends State<_PeerCard> } Widget _buildLandscape() { - final PeerTabModel peerTabModel = Provider.of(context); final peer = super.widget.peer; var deco = Rx( BoxDecoration( @@ -115,30 +120,21 @@ class _PeerCardState extends State<_PeerCard> ), ); }, - child: GestureDetector( - onDoubleTap: - peerTabModel.multiSelectionMode || peerTabModel.isShiftDown - ? null - : () => widget.connect(context, peer.id), - onTap: () => peerTabModel.select(peer), - onLongPress: () => peerTabModel.select(peer), + child: gestureDetector( child: Obx(() => peerCardUiType.value == PeerUiType.grid ? _buildPeerCard(context, peer, deco) : _buildPeerTile(context, peer, deco))), ); } - Widget _buildPeerTile( - BuildContext context, Peer peer, Rx? deco) { - hideUsernameOnCard ??= - bind.mainGetBuildinOption(key: kHideUsernameOnCard) == 'Y'; + makeChild(bool isPortrait, Peer peer) { final name = hideUsernameOnCard == true ? peer.hostname : '${peer.username}${peer.username.isNotEmpty && peer.hostname.isNotEmpty ? '@' : ''}${peer.hostname}'; final greyStyle = TextStyle( fontSize: 11, color: Theme.of(context).textTheme.titleLarge?.color?.withOpacity(0.6)); - makeChild(bool isPortrait) => Row( + return Row( mainAxisSize: MainAxisSize.max, children: [ Container( @@ -210,6 +206,12 @@ class _PeerCardState extends State<_PeerCard> ) ], ); + } + + Widget _buildPeerTile( + BuildContext context, Peer peer, Rx? deco) { + hideUsernameOnCard ??= + bind.mainGetBuildinOption(key: kHideUsernameOnCard) == 'Y'; final colors = _frontN(peer.tags, 25) .map((e) => gFFI.abModel.getCurrentAbTagColor(e)) .toList(); @@ -220,21 +222,22 @@ class _PeerCardState extends State<_PeerCard> ? '${translate('Tags')}: ${peer.tags.join(', ')}' : '', child: Stack(children: [ - Obx(() => deco == null - ? makeChild(stateGlobal.isPortrait.isTrue) - : Container( + Obx( + () => deco == null + ? makeChild(stateGlobal.isPortrait.isTrue, peer) + : Container( foregroundDecoration: deco.value, - child: makeChild(stateGlobal.isPortrait.isTrue), + child: makeChild(stateGlobal.isPortrait.isTrue, peer), ), - ), + ), if (colors.isNotEmpty) - Obx(()=> Positioned( - top: 2, - right: stateGlobal.isPortrait.isTrue ? 20 : 10, - child: CustomPaint( - painter: TagPainter(radius: 3, colors: colors), - ), - )) + Obx(() => Positioned( + top: 2, + right: stateGlobal.isPortrait.isTrue ? 20 : 10, + child: CustomPaint( + painter: TagPainter(radius: 3, colors: colors), + ), + )) ]), ); } @@ -1259,54 +1262,53 @@ void _rdpDialog(String id) async { ], ).marginOnly(bottom: isDesktop ? 8 : 0), Obx(() => Row( - children: [ - stateGlobal.isPortrait.isFalse - ? ConstrainedBox( - constraints: const BoxConstraints(minWidth: 140), - child: Text( - "${translate('Username')}:", - textAlign: TextAlign.right, - ).marginOnly(right: 10)) - : SizedBox.shrink(), - Expanded( - child: TextField( - decoration: InputDecoration( - labelText: isDesktop - ? null - : translate('Username')), - controller: userController, - ), - ), - ], - ).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, + children: [ + stateGlobal.isPortrait.isFalse + ? ConstrainedBox( + constraints: const BoxConstraints(minWidth: 140), + child: Text( + "${translate('Username')}:", + textAlign: TextAlign.right, + ).marginOnly(right: 10)) + : SizedBox.shrink(), + Expanded( + child: TextField( 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, - )), - ), - ], - )) + labelText: + isDesktop ? null : translate('Username')), + controller: userController, + ), + ), + ], + ).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, + )), + ), + ], + )) ], ), ), diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index dc02ac81fdac..9342c9e5086c 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -475,7 +475,8 @@ class _AppState extends State with WidgetsBindingObserver { : (context, child) { child = _keepScaleBuilder(context, child); child = botToastBuilder(context, child); - if (isDesktop && desktopType == DesktopType.main) { + if ((isDesktop && desktopType == DesktopType.main) || + isWebDesktop) { child = keyListenerBuilder(context, child); } if (isLinux) { diff --git a/flutter/lib/models/peer_tab_model.dart b/flutter/lib/models/peer_tab_model.dart index 7dab2574dcf5..fbde560c2040 100644 --- a/flutter/lib/models/peer_tab_model.dart +++ b/flutter/lib/models/peer_tab_model.dart @@ -152,7 +152,7 @@ class PeerTabModel with ChangeNotifier { // https://github.com/flutter/flutter/issues/101275#issuecomment-1604541700 // After onTap, the shift key should be pressed for a while when not in multiselection mode, // because onTap is delayed when onDoubleTap is not null - if (isDesktop && !_isShiftDown) return; + if ((isDesktop || isWebDesktop) && !_isShiftDown) return; _multiSelectionMode = true; } final cached = _currentTabCachedPeers.map((e) => e.id).toList(); From 29b01e9cef48b3278cf7f1fee44368139874d0dd Mon Sep 17 00:00:00 2001 From: 21pages Date: Fri, 11 Oct 2024 16:47:08 +0800 Subject: [PATCH 283/541] remove shift & tap enable multiselect (#9625) Signed-off-by: 21pages --- flutter/lib/common/widgets/peer_card.dart | 2 +- flutter/lib/models/peer_tab_model.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/flutter/lib/common/widgets/peer_card.dart b/flutter/lib/common/widgets/peer_card.dart index 246b337a98b7..8dd54fb1a737 100644 --- a/flutter/lib/common/widgets/peer_card.dart +++ b/flutter/lib/common/widgets/peer_card.dart @@ -62,7 +62,7 @@ class _PeerCardState extends State<_PeerCard> final PeerTabModel peerTabModel = Provider.of(context); final peer = super.widget.peer; return GestureDetector( - onDoubleTap: peerTabModel.multiSelectionMode || peerTabModel.isShiftDown + onDoubleTap: peerTabModel.multiSelectionMode ? null : () => widget.connect(context, peer.id), onTap: () { diff --git a/flutter/lib/models/peer_tab_model.dart b/flutter/lib/models/peer_tab_model.dart index fbde560c2040..83df1f05d6aa 100644 --- a/flutter/lib/models/peer_tab_model.dart +++ b/flutter/lib/models/peer_tab_model.dart @@ -152,7 +152,7 @@ class PeerTabModel with ChangeNotifier { // https://github.com/flutter/flutter/issues/101275#issuecomment-1604541700 // After onTap, the shift key should be pressed for a while when not in multiselection mode, // because onTap is delayed when onDoubleTap is not null - if ((isDesktop || isWebDesktop) && !_isShiftDown) return; + if (isDesktop || isWebDesktop) return; _multiSelectionMode = true; } final cached = _currentTabCachedPeers.map((e) => e.id).toList(); From eb1ef0969cd7bc0f48528c903eb6e84cf30336f2 Mon Sep 17 00:00:00 2001 From: 21pages Date: Sat, 12 Oct 2024 09:03:13 +0800 Subject: [PATCH 284/541] web file transfer (#9587) Signed-off-by: 21pages --- flutter/lib/common.dart | 36 +- flutter/lib/common/widgets/peer_card.dart | 2 +- .../lib/desktop/pages/file_manager_page.dart | 103 +++-- flutter/lib/models/file_model.dart | 43 +- flutter/lib/models/model.dart | 4 + flutter/lib/web/bridge.dart | 370 ++++++++++-------- flutter/lib/web/dummy.dart | 14 + flutter/lib/web/web_unique.dart | 30 ++ src/flutter_ffi.rs | 3 +- 9 files changed, 411 insertions(+), 194 deletions(-) create mode 100644 flutter/lib/web/dummy.dart create mode 100644 flutter/lib/web/web_unique.dart diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 8feb2402f8a5..881cf3b8b98f 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -30,6 +30,7 @@ import 'common/widgets/overlay.dart'; import 'mobile/pages/file_manager_page.dart'; import 'mobile/pages/remote_page.dart'; import 'desktop/pages/remote_page.dart' as desktop_remote; +import 'desktop/pages/file_manager_page.dart' as desktop_file_manager; import 'package:flutter_hbb/desktop/widgets/remote_toolbar.dart'; import 'models/model.dart'; import 'models/platform_model.dart'; @@ -2370,18 +2371,33 @@ connect(BuildContext context, String id, } } else { if (isFileTransfer) { - if (!await AndroidPermissionManager.check(kManageExternalStorage)) { - if (!await AndroidPermissionManager.request(kManageExternalStorage)) { - return; + if (isAndroid) { + if (!await AndroidPermissionManager.check(kManageExternalStorage)) { + if (!await AndroidPermissionManager.request(kManageExternalStorage)) { + return; + } } } - Navigator.push( - context, - MaterialPageRoute( - builder: (BuildContext context) => FileManagerPage( - id: id, password: password, isSharedPassword: isSharedPassword), - ), - ); + if (isWeb) { + Navigator.push( + context, + MaterialPageRoute( + builder: (BuildContext context) => + desktop_file_manager.FileManagerPage( + id: id, + password: password, + isSharedPassword: isSharedPassword), + ), + ); + } else { + Navigator.push( + context, + MaterialPageRoute( + builder: (BuildContext context) => FileManagerPage( + id: id, password: password, isSharedPassword: isSharedPassword), + ), + ); + } } else { if (isWeb) { Navigator.push( diff --git a/flutter/lib/common/widgets/peer_card.dart b/flutter/lib/common/widgets/peer_card.dart index 8dd54fb1a737..690c829775f7 100644 --- a/flutter/lib/common/widgets/peer_card.dart +++ b/flutter/lib/common/widgets/peer_card.dart @@ -879,7 +879,7 @@ class RecentPeerCard extends BasePeerCard { BuildContext context) async { final List> menuItems = [ _connectAction(context), - if (!isWeb) _transferFileAction(context), + _transferFileAction(context), ]; final List favs = (await bind.mainGetFav()).toList(); diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index b75a946c06be..5557783b5c76 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -17,6 +17,8 @@ import 'package:flutter_hbb/models/file_model.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.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 '../../desktop/widgets/material_mod_popup_menu.dart' as mod_menu; @@ -55,14 +57,14 @@ class FileManagerPage extends StatefulWidget { required this.id, required this.password, required this.isSharedPassword, - required this.tabController, + this.tabController, this.forceRelay}) : super(key: key); final String id; final String? password; final bool? isSharedPassword; final bool? forceRelay; - final DesktopTabController tabController; + final DesktopTabController? tabController; @override State createState() => _FileManagerPageState(); @@ -97,11 +99,14 @@ class _FileManagerPageState extends State if (!isLinux) { WakelockPlus.enable(); } + if (isWeb) { + _ffi.ffiModel.updateEventListener(_ffi.sessionId, widget.id); + } debugPrint("File manager page init success with id ${widget.id}"); _ffi.dialogManager.setOverlayState(_overlayKeyState); // 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); + widget.tabController?.onSelected?.call(widget.id); }); WidgetsBinding.instance.addObserver(this); } @@ -140,10 +145,11 @@ class _FileManagerPageState extends State backgroundColor: Theme.of(context).scaffoldBackgroundColor, body: Row( children: [ - Flexible( - flex: 3, - child: dropArea(FileManagerView( - model.localController, _ffi, _mouseFocusScope))), + if (!isWeb) + Flexible( + flex: 3, + child: dropArea(FileManagerView( + model.localController, _ffi, _mouseFocusScope))), Flexible( flex: 3, child: dropArea(FileManagerView( @@ -192,7 +198,13 @@ class _FileManagerPageState extends State return Icon(Icons.delete_outline, color: color); default: return Transform.rotate( - angle: job.isRemoteToLocal ? pi : 0, + angle: isWeb + ? job.isRemoteToLocal + ? pi / 2 + : pi / 2 * 3 + : job.isRemoteToLocal + ? pi + : 0, child: Icon(Icons.arrow_forward_ios, color: color), ); } @@ -800,6 +812,50 @@ class _FileManagerViewState extends State { ], ), ), + if (isWeb) + Obx(() => ElevatedButton.icon( + style: ButtonStyle( + padding: MaterialStateProperty.all( + isLocal + ? EdgeInsets.only(left: 10) + : EdgeInsets.only(right: 10)), + backgroundColor: MaterialStateProperty.all( + selectedItems.items.isEmpty + ? MyTheme.accent80 + : MyTheme.accent, + ), + ), + onPressed: () => {webselectFiles(is_folder: true)}, + icon: Offstage(), + label: Text( + translate('Upload folder'), + textAlign: TextAlign.right, + style: TextStyle( + color: Colors.white, + ), + ))).marginOnly(left: 16), + if (isWeb) + Obx(() => ElevatedButton.icon( + style: ButtonStyle( + padding: MaterialStateProperty.all( + isLocal + ? EdgeInsets.only(left: 10) + : EdgeInsets.only(right: 10)), + backgroundColor: MaterialStateProperty.all( + selectedItems.items.isEmpty + ? MyTheme.accent80 + : MyTheme.accent, + ), + ), + onPressed: () => {webselectFiles(is_folder: false)}, + icon: Offstage(), + label: Text( + translate('Upload files'), + textAlign: TextAlign.right, + style: TextStyle( + color: Colors.white, + ), + ))).marginOnly(left: 16), Obx(() => ElevatedButton.icon( style: ButtonStyle( padding: MaterialStateProperty.all( @@ -833,19 +889,22 @@ class _FileManagerViewState extends State { : Colors.white, ), ) - : RotatedBox( - quarterTurns: 2, - child: SvgPicture.asset( - "assets/arrow.svg", - colorFilter: svgColor(selectedItems.items.isEmpty - ? Theme.of(context).brightness == - Brightness.light - ? MyTheme.grayBg - : MyTheme.darkGray - : Colors.white), - alignment: Alignment.bottomRight, - ), - ), + : isWeb + ? Offstage() + : RotatedBox( + quarterTurns: 2, + child: SvgPicture.asset( + "assets/arrow.svg", + colorFilter: svgColor( + selectedItems.items.isEmpty + ? Theme.of(context).brightness == + Brightness.light + ? MyTheme.grayBg + : MyTheme.darkGray + : Colors.white), + alignment: Alignment.bottomRight, + ), + ), label: isLocal ? SvgPicture.asset( "assets/arrow.svg", @@ -857,7 +916,7 @@ class _FileManagerViewState extends State { : Colors.white), ) : Text( - translate('Receive'), + translate(isWeb ? 'Download' : 'Receive'), style: TextStyle( color: selectedItems.items.isEmpty ? Theme.of(context).brightness == diff --git a/flutter/lib/models/file_model.dart b/flutter/lib/models/file_model.dart index a0d5bc0b5588..05c79ae86a75 100644 --- a/flutter/lib/models/file_model.dart +++ b/flutter/lib/models/file_model.dart @@ -7,6 +7,8 @@ import 'package:flutter_hbb/common/widgets/dialog.dart'; import 'package:flutter_hbb/utils/event_loop.dart'; import 'package:get/get.dart'; import 'package:path/path.dart' as path; +import 'package:flutter_hbb/web/dummy.dart' + if (dart.library.html) 'package:flutter_hbb/web/web_unique.dart'; import '../consts.dart'; import 'model.dart'; @@ -74,7 +76,7 @@ class FileModel { Future onReady() async { await evtLoop.onReady(); - await localController.onReady(); + if (!isWeb) await localController.onReady(); await remoteController.onReady(); } @@ -86,7 +88,7 @@ class FileModel { } Future refreshAll() async { - await localController.refresh(); + if (!isWeb) await localController.refresh(); await remoteController.refresh(); } @@ -228,6 +230,33 @@ class FileModel { ); }, useAnimation: false); } + + void onSelectedFiles(dynamic obj) { + localController.selectedItems.clear(); + + try { + int handleIndex = int.parse(obj['handleIndex']); + final file = jsonDecode(obj['file']); + var entry = Entry.fromJson(file); + entry.path = entry.name; + final otherSideData = remoteController.directoryData(); + final toPath = otherSideData.directory.path; + final isWindows = otherSideData.options.isWindows; + final showHidden = otherSideData.options.showHidden; + final jobID = jobController.addTransferJob(entry, false); + webSendLocalFiles( + handleIndex: handleIndex, + actId: jobID, + path: entry.path, + to: PathUtil.join(toPath, entry.name, isWindows), + fileNum: 0, + includeHidden: showHidden, + isRemote: false, + ); + } catch (e) { + debugPrint("Failed to decode onSelectedFiles: $e"); + } + } } class DirectoryData { @@ -462,7 +491,8 @@ class FileController { to: PathUtil.join(toPath, from.name, isWindows), fileNum: 0, includeHidden: showHidden, - isRemote: isRemoteToLocal); + isRemote: isRemoteToLocal, + isDir: from.isDirectory); debugPrint( "path: ${from.path}, toPath: $toPath, to: ${PathUtil.join(toPath, from.name, isWindows)}"); } @@ -489,7 +519,7 @@ class FileController { } else if (item.isDirectory) { title = translate("Not an empty directory"); dialogManager?.showLoading(translate("Waiting")); - final fd = await fileFetcher.fetchDirectoryRecursive( + final fd = await fileFetcher.fetchDirectoryRecursiveToRemove( jobID, item.path, items.isLocal, true); if (fd.path.isEmpty) { fd.path = item.path; @@ -809,7 +839,6 @@ class JobController { job.speed = double.parse(evt['speed']); job.finishedSize = int.parse(evt['finished_size']); job.recvJobRes = true; - debugPrint("update job $id with $evt"); jobTable.refresh(); } } catch (e) { @@ -1116,11 +1145,11 @@ class FileFetcher { } } - Future fetchDirectoryRecursive( + Future fetchDirectoryRecursiveToRemove( int actID, String path, bool isLocal, bool showHidden) async { // TODO test Recursive is show hidden default? try { - await bind.sessionReadDirRecursive( + await bind.sessionReadDirToRemoveRecursive( sessionId: sessionId, actId: actID, path: path, diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 8466443ee80b..2833f38349e4 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -390,6 +390,10 @@ class FfiModel with ChangeNotifier { handleFollowCurrentDisplay(evt, sessionId, peerId); } else if (name == 'use_texture_render') { _handleUseTextureRender(evt, sessionId, peerId); + } else if (name == "selected_files") { + if (isWeb) { + parent.target?.fileModel.onSelectedFiles(evt); + } } else { debugPrint('Event is not handled in the fixed branch: $name'); } diff --git a/flutter/lib/web/bridge.dart b/flutter/lib/web/bridge.dart index adcbadb30e97..0362490cdf10 100644 --- a/flutter/lib/web/bridge.dart +++ b/flutter/lib/web/bridge.dart @@ -52,12 +52,12 @@ class EventToUI_Texture implements EventToUI { class RustdeskImpl { Future stopGlobalEventStream({required String appType, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("stopGlobalEventStream"); } Future hostStopSystemKeyPropagate( {required bool stopped, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("hostStopSystemKeyPropagate"); } int peerGetDefaultSessionsCount({required String id, dynamic hint}) { @@ -88,7 +88,8 @@ class RustdeskImpl { jsonEncode({ 'id': id, 'password': password, - 'is_shared_password': isSharedPassword + 'is_shared_password': isSharedPassword, + 'isFileTransfer': isFileTransfer }) ]); } @@ -107,7 +108,7 @@ class RustdeskImpl { required String id, required Int32List displays, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("sessionStartWithDisplays"); } Future sessionGetRemember( @@ -178,12 +179,12 @@ class RustdeskImpl { required int width, required int height, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("sessionRecordScreen"); } Future sessionRecordStatus( {required UuidValue sessionId, required bool status, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("sessionRecordStatus"); } Future sessionReconnect( @@ -435,7 +436,7 @@ class RustdeskImpl { required int lockModes, required bool downOrUp, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("sessionHandleFlutterRawKeyEvent"); } void sessionEnterOrLeave( @@ -507,7 +508,10 @@ class RustdeskImpl { required String path, required bool includeHidden, dynamic hint}) { - throw UnimplementedError(); + return Future(() => js.context.callMethod('setByName', [ + 'read_remote_dir', + jsonEncode({'path': path, 'include_hidden': includeHidden}) + ])); } Future sessionSendFiles( @@ -518,8 +522,20 @@ class RustdeskImpl { required int fileNum, required bool includeHidden, required bool isRemote, + required bool isDir, dynamic hint}) { - throw UnimplementedError(); + return Future(() => js.context.callMethod('setByName', [ + 'send_files', + jsonEncode({ + 'id': actId, + 'path': path, + 'to': to, + 'file_num': fileNum, + 'include_hidden': includeHidden, + 'is_remote': isRemote, + 'is_dir': isDir, + }) + ])); } Future sessionSetConfirmOverrideFile( @@ -530,7 +546,16 @@ class RustdeskImpl { required bool remember, required bool isUpload, dynamic hint}) { - throw UnimplementedError(); + return Future(() => js.context.callMethod('setByName', [ + 'confirm_override_file', + jsonEncode({ + 'id': actId, + 'file_num': fileNum, + 'need_override': needOverride, + 'remember': remember, + 'is_upload': isUpload + }) + ])); } Future sessionRemoveFile( @@ -540,17 +565,33 @@ class RustdeskImpl { required int fileNum, required bool isRemote, dynamic hint}) { - throw UnimplementedError(); + return Future(() => js.context.callMethod('setByName', [ + 'remove_file', + jsonEncode({ + 'id': actId, + 'path': path, + 'file_num': fileNum, + 'is_remote': isRemote + }) + ])); } - Future sessionReadDirRecursive( + Future sessionReadDirToRemoveRecursive( {required UuidValue sessionId, required int actId, required String path, required bool isRemote, required bool showHidden, dynamic hint}) { - throw UnimplementedError(); + return Future(() => js.context.callMethod('setByName', [ + 'read_dir_to_remove_recursive', + jsonEncode({ + 'id': actId, + 'path': path, + 'is_remote': isRemote, + 'show_hidden': showHidden + }) + ])); } Future sessionRemoveAllEmptyDirs( @@ -559,12 +600,16 @@ class RustdeskImpl { required String path, required bool isRemote, dynamic hint}) { - throw UnimplementedError(); + return Future(() => js.context.callMethod('setByName', [ + 'remove_all_empty_dirs', + jsonEncode({'id': actId, 'path': path, 'is_remote': isRemote}) + ])); } Future sessionCancelJob( {required UuidValue sessionId, required int actId, dynamic hint}) { - throw UnimplementedError(); + return Future( + () => js.context.callMethod('setByName', ['cancel_job', actId])); } Future sessionCreateDir( @@ -573,7 +618,10 @@ class RustdeskImpl { required String path, required bool isRemote, dynamic hint}) { - throw UnimplementedError(); + return Future(() => js.context.callMethod('setByName', [ + 'create_dir', + jsonEncode({'id': actId, 'path': path, 'is_remote': isRemote}) + ])); } Future sessionReadLocalDirSync( @@ -581,17 +629,21 @@ class RustdeskImpl { required String path, required bool showHidden, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("sessionReadLocalDirSync"); } Future sessionGetPlatform( {required UuidValue sessionId, required bool isRemote, dynamic hint}) { - throw UnimplementedError(); + if (isRemote) { + return Future(() => js.context.callMethod('getByName', ['platform'])); + } else { + return Future(() => 'Web'); + } } Future sessionLoadLastTransferJobs( {required UuidValue sessionId, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("sessionLoadLastTransferJobs"); } Future sessionAddJob( @@ -603,7 +655,7 @@ class RustdeskImpl { required bool includeHidden, required bool isRemote, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("sessionAddJob"); } Future sessionResumeJob( @@ -611,7 +663,7 @@ class RustdeskImpl { required int actId, required bool isRemote, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("sessionResumeJob"); } Future sessionElevateDirect( @@ -632,7 +684,7 @@ class RustdeskImpl { Future sessionSwitchSides( {required UuidValue sessionId, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("sessionSwitchSides"); } Future sessionChangeResolution( @@ -642,7 +694,7 @@ class RustdeskImpl { required int height, dynamic hint}) { // note: restore on disconnected - throw UnimplementedError(); + throw UnimplementedError("sessionChangeResolution"); } Future sessionSetSize( @@ -656,15 +708,15 @@ class RustdeskImpl { Future sessionSendSelectedSessionId( {required UuidValue sessionId, required String sid, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("sessionSendSelectedSessionId"); } Future> mainGetSoundInputs({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainGetSoundInputs"); } Future mainGetDefaultSoundInput({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainGetDefaultSoundInput"); } String mainGetLoginDeviceInfo({dynamic hint}) { @@ -680,11 +732,11 @@ class RustdeskImpl { } Future mainChangeId({required String newId, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainChangeId"); } Future mainGetAsyncStatus({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainGetAsyncStatus"); } Future mainGetOption({required String key, dynamic hint}) { @@ -696,11 +748,11 @@ class RustdeskImpl { } Future mainGetError({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainGetError"); } bool mainShowOption({required String key, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainShowOption"); } Future mainSetOption( @@ -737,11 +789,11 @@ class RustdeskImpl { required String username, required String password, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainSetSocks"); } Future> mainGetSocks({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainGetSocks"); } Future mainGetAppName({dynamic hint}) { @@ -753,7 +805,7 @@ class RustdeskImpl { } String mainUriPrefixSync({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainUriPrefixSync"); } Future mainGetLicense({dynamic hint}) { @@ -785,11 +837,11 @@ class RustdeskImpl { String mainGetPeerSync({required String id, dynamic hint}) { // TODO: - throw UnimplementedError(); + throw UnimplementedError("mainGetPeerSync"); } Future mainGetLanPeers({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainGetLanPeers"); } Future mainGetConnectStatus({dynamic hint}) { @@ -798,7 +850,7 @@ class RustdeskImpl { } Future mainCheckConnectStatus({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainCheckConnectStatus"); } Future mainIsUsingPublicServer({dynamic hint}) { @@ -808,7 +860,7 @@ class RustdeskImpl { } Future mainDiscover({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainDiscover"); } Future mainGetApiServer({dynamic hint}) { @@ -820,7 +872,7 @@ class RustdeskImpl { required String body, required String header, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainPostRequest"); } Future mainGetProxyStatus({dynamic hint}) { @@ -834,11 +886,11 @@ class RustdeskImpl { required String header, dynamic hint, }) { - throw UnimplementedError(); + throw UnimplementedError("mainHttpRequest"); } Future mainGetHttpStatus({required String url, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainGetHttpStatus"); } String mainGetLocalOption({required String key, dynamic hint}) { @@ -846,7 +898,7 @@ class RustdeskImpl { } String mainGetEnv({required String key, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainGetEnv"); } Future mainSetLocalOption( @@ -940,7 +992,7 @@ class RustdeskImpl { } Future mainGetNewStoredPeers({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainGetNewStoredPeers"); } Future mainForgetPassword({required String id, dynamic hint}) { @@ -973,7 +1025,7 @@ class RustdeskImpl { Future mainLoadRecentPeersForAb( {required String filter, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainLoadRecentPeersForAb"); } Future mainLoadFavPeers({dynamic hint}) { @@ -981,23 +1033,23 @@ class RustdeskImpl { } Future mainLoadLanPeers({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainLoadLanPeers"); } Future mainRemoveDiscovered({required String id, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainRemoveDiscovered"); } Future mainChangeTheme({required String dark, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainChangeTheme"); } Future mainChangeLanguage({required String lang, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainChangeLanguage"); } String mainVideoSaveDirectory({required bool root, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainVideoSaveDirectory"); } Future mainSetUserDefaultOption( @@ -1026,7 +1078,7 @@ class RustdeskImpl { } String mainGetDisplays({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainGetDisplays"); } Future sessionAddPortForward( @@ -1035,35 +1087,35 @@ class RustdeskImpl { required String remoteHost, required int remotePort, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("sessionAddPortForward"); } Future sessionRemovePortForward( {required UuidValue sessionId, required int localPort, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("sessionRemovePortForward"); } Future sessionNewRdp({required UuidValue sessionId, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("sessionNewRdp"); } Future sessionRequestVoiceCall( {required UuidValue sessionId, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("sessionRequestVoiceCall"); } Future sessionCloseVoiceCall( {required UuidValue sessionId, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("sessionCloseVoiceCall"); } Future cmHandleIncomingVoiceCall( {required int id, required bool accept, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("cmHandleIncomingVoiceCall"); } Future cmCloseVoiceCall({required int id, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("cmCloseVoiceCall"); } Future mainGetLastRemoteId({dynamic hint}) { @@ -1072,7 +1124,7 @@ class RustdeskImpl { } Future mainGetSoftwareUpdateUrl({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainGetSoftwareUpdateUrl"); } Future mainGetHomeDir({dynamic hint}) { @@ -1096,15 +1148,15 @@ class RustdeskImpl { } Future cmGetClientsState({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("cmGetClientsState"); } Future cmCheckClientsLength({required int length, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("cmCheckClientsLength"); } Future cmGetClientsLength({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("cmCheckClientsLength"); } Future mainInit({required String appDir, dynamic hint}) { @@ -1113,12 +1165,12 @@ class RustdeskImpl { Future mainDeviceId({required String id, dynamic hint}) { // TODO: ? - throw UnimplementedError(); + throw UnimplementedError("mainDeviceId"); } Future mainDeviceName({required String name, dynamic hint}) { // TODO: ? - throw UnimplementedError(); + throw UnimplementedError("mainDeviceName"); } Future mainRemovePeer({required String id, dynamic hint}) { @@ -1127,11 +1179,11 @@ class RustdeskImpl { } bool mainHasHwcodec({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainHasHwcodec"); } bool mainHasVram({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainHasVram"); } String mainSupportedHwdecodings({dynamic hint}) { @@ -1139,15 +1191,15 @@ class RustdeskImpl { } Future mainIsRoot({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainIsRoot"); } int getDoubleClickTime({dynamic hint}) { - throw UnimplementedError(); + return 500; } Future mainStartDbusServer({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainStartDbusServer"); } Future mainSaveAb({required String json, dynamic hint}) { @@ -1177,7 +1229,7 @@ class RustdeskImpl { Future sessionSendPointer( {required UuidValue sessionId, required String msg, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("sessionSendPointer"); } Future sessionSendMouse( @@ -1231,76 +1283,75 @@ class RustdeskImpl { } Future mainSetHomeDir({required String home, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainSetHomeDir"); } String mainGetDataDirIos({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainGetDataDirIos"); } Future mainStopService({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainStopService"); } Future mainStartService({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainStartService"); } Future mainUpdateTemporaryPassword({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainUpdateTemporaryPassword"); } Future mainSetPermanentPassword( {required String password, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainSetPermanentPassword"); } Future mainCheckSuperUserPermission({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainCheckSuperUserPermission"); } Future mainCheckMouseTime({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainCheckMouseTime"); } Future mainGetMouseTime({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainGetMouseTime"); } Future mainWol({required String id, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainWol"); } Future mainCreateShortcut({required String id, dynamic hint}) { - // TODO: - throw UnimplementedError(); + throw UnimplementedError("mainCreateShortcut"); } Future cmSendChat( {required int connId, required String msg, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("cmSendChat"); } Future cmLoginRes( {required int connId, required bool res, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("cmLoginRes"); } Future cmCloseConnection({required int connId, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("cmCloseConnection"); } Future cmRemoveDisconnectedConnection( {required int connId, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("cmRemoveDisconnectedConnection"); } Future cmCheckClickTime({required int connId, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("cmCheckClickTime"); } Future cmGetClickTime({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("cmGetClickTime"); } Future cmSwitchPermission( @@ -1308,23 +1359,23 @@ class RustdeskImpl { required String name, required bool enabled, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("cmSwitchPermission"); } bool cmCanElevate({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("cmCanElevate"); } Future cmElevatePortable({required int connId, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("cmElevatePortable"); } Future cmSwitchBack({required int connId, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("cmSwitchBack"); } Future cmGetConfig({required String name, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("cmGetConfig"); } Future mainGetBuildDate({dynamic hint}) { @@ -1377,89 +1428,89 @@ class RustdeskImpl { } bool mainIsInstalled({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainIsInstalled"); } void mainInitInputSource({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainIsInstalled"); } bool mainIsInstalledLowerVersion({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainIsInstalledLowerVersion"); } bool mainIsInstalledDaemon({required bool prompt, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainIsInstalledDaemon"); } bool mainIsProcessTrusted({required bool prompt, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainIsProcessTrusted"); } bool mainIsCanScreenRecording({required bool prompt, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainIsCanScreenRecording"); } bool mainIsCanInputMonitoring({required bool prompt, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainIsCanInputMonitoring"); } bool mainIsShareRdp({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainIsShareRdp"); } Future mainSetShareRdp({required bool enable, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainSetShareRdp"); } bool mainGotoInstall({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainGotoInstall"); } String mainGetNewVersion({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainGetNewVersion"); } bool mainUpdateMe({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainUpdateMe"); } Future setCurSessionId({required UuidValue sessionId, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("setCurSessionId"); } bool installShowRunWithoutInstall({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("installShowRunWithoutInstall"); } Future installRunWithoutInstall({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("installRunWithoutInstall"); } Future installInstallMe( {required String options, required String path, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("installInstallMe"); } String installInstallPath({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("installInstallPath"); } Future mainAccountAuth( {required String op, required bool rememberMe, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainAccountAuth"); } Future mainAccountAuthCancel({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainAccountAuthCancel"); } Future mainAccountAuthResult({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainAccountAuthResult"); } Future mainOnMainWindowClose({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainOnMainWindowClose"); } bool mainCurrentIsWayland({dynamic hint}) { @@ -1471,7 +1522,7 @@ class RustdeskImpl { } bool mainHideDock({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainHideDock"); } bool mainHasFileClipboard({dynamic hint}) { @@ -1483,11 +1534,11 @@ class RustdeskImpl { } Future cmInit({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("cmInit"); } Future mainStartIpcUrlServer({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainStartIpcUrlServer"); } Future mainTestWallpaper({required int second, dynamic hint}) { @@ -1537,7 +1588,7 @@ class RustdeskImpl { } Future sendUrlScheme({required String url, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("sendUrlScheme"); } Future pluginEvent( @@ -1545,12 +1596,12 @@ class RustdeskImpl { required String peer, required Uint8List event, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("pluginEvent"); } Stream pluginRegisterEventStream( {required String id, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("pluginRegisterEventStream"); } String? pluginGetSessionOption( @@ -1558,7 +1609,7 @@ class RustdeskImpl { required String peer, required String key, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("pluginGetSessionOption"); } Future pluginSetSessionOption( @@ -1567,12 +1618,12 @@ class RustdeskImpl { required String key, required String value, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("pluginSetSessionOption"); } String? pluginGetSharedOption( {required String id, required String key, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("pluginGetSharedOption"); } Future pluginSetSharedOption( @@ -1580,36 +1631,36 @@ class RustdeskImpl { required String key, required String value, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("pluginSetSharedOption"); } Future pluginReload({required String id, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("pluginReload"); } void pluginEnable({required String id, required bool v, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("pluginEnable"); } bool pluginIsEnabled({required String id, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("pluginIsEnabled"); } bool pluginFeatureIsEnabled({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("pluginFeatureIsEnabled"); } Future pluginSyncUi({required String syncTo, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("pluginSyncUi"); } Future pluginListReload({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("pluginListReload"); } Future pluginInstall( {required String id, required bool b, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("pluginInstall"); } bool isSupportMultiUiSession({required String version, dynamic hint}) { @@ -1621,11 +1672,11 @@ class RustdeskImpl { } String mainDefaultPrivacyModeImpl({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainDefaultPrivacyModeImpl"); } String mainSupportedPrivacyModeImpls({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainSupportedPrivacyModeImpls"); } String mainSupportedInputSource({dynamic hint}) { @@ -1636,33 +1687,33 @@ class RustdeskImpl { } Future mainGenerate2Fa({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainGenerate2Fa"); } Future mainVerify2Fa({required String code, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainVerify2Fa"); } bool mainHasValid2FaSync({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainHasValid2FaSync"); } String mainGetHardOption({required String key, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainGetHardOption"); } Future mainCheckHwcodec({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainCheckHwcodec"); } Future sessionRequestNewDisplayInitMsgs( {required UuidValue sessionId, required int display, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("sessionRequestNewDisplayInitMsgs"); } Future mainHandleWaylandScreencastRestoreToken( {required String key, required String value, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainHandleWaylandScreencastRestoreToken"); } bool mainIsOptionFixed({required String key, dynamic hint}) { @@ -1670,23 +1721,23 @@ class RustdeskImpl { } bool mainGetUseTextureRender({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainGetUseTextureRender"); } bool mainHasValidBotSync({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainHasValidBotSync"); } Future mainVerifyBot({required String token, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainVerifyBot"); } String mainGetUnlockPin({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainGetUnlockPin"); } String mainSetUnlockPin({required String pin, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainSetUnlockPin"); } bool sessionGetEnableTrustedDevices( @@ -1696,28 +1747,28 @@ class RustdeskImpl { } Future mainGetTrustedDevices({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainGetTrustedDevices"); } Future mainRemoveTrustedDevices({required String json, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainRemoveTrustedDevices"); } Future mainClearTrustedDevices({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainClearTrustedDevices"); } Future getVoiceCallInputDevice({required bool isCm, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("getVoiceCallInputDevice"); } Future setVoiceCallInputDevice( {required bool isCm, required String device, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("setVoiceCallInputDevice"); } bool isPresetPasswordMobileOnly({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("isPresetPasswordMobileOnly"); } String mainGetBuildinOption({required String key, dynamic hint}) { @@ -1725,21 +1776,34 @@ class RustdeskImpl { } String installInstallOptions({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("installInstallOptions"); } int mainMaxEncryptLen({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainMaxEncryptLen"); } - sessionRenameFile( + Future sessionRenameFile( {required UuidValue sessionId, required int actId, required String path, required String newName, required bool isRemote, dynamic hint}) { - throw UnimplementedError(); + return Future(() => js.context.callMethod('setByName', [ + 'rename_file', + jsonEncode({ + 'id': actId, + 'path': path, + 'new_name': newName, + 'is_remote': isRemote + }) + ])); + } + + Future sessionSelectFiles( + {required UuidValue sessionId, dynamic hint}) { + return Future(() => js.context.callMethod('setByName', ['select_files'])); } void dispose() {} diff --git a/flutter/lib/web/dummy.dart b/flutter/lib/web/dummy.dart new file mode 100644 index 000000000000..b9e3b80b6ed4 --- /dev/null +++ b/flutter/lib/web/dummy.dart @@ -0,0 +1,14 @@ +Future webselectFiles({required bool is_folder}) async { + throw UnimplementedError("webselectFiles"); +} + +Future webSendLocalFiles( + {required int handleIndex, + required int actId, + required String path, + required String to, + required int fileNum, + required bool includeHidden, + required bool isRemote}) { + throw UnimplementedError("webSendLocalFiles"); +} diff --git a/flutter/lib/web/web_unique.dart b/flutter/lib/web/web_unique.dart new file mode 100644 index 000000000000..14774e668b0c --- /dev/null +++ b/flutter/lib/web/web_unique.dart @@ -0,0 +1,30 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:js' as js; + +Future webselectFiles({required bool is_folder}) async { + return Future( + () => js.context.callMethod('setByName', ['select_files', is_folder])); +} + +Future webSendLocalFiles( + {required int handleIndex, + required int actId, + required String path, + required String to, + required int fileNum, + required bool includeHidden, + required bool isRemote}) { + return Future(() => js.context.callMethod('setByName', [ + 'send_local_files', + jsonEncode({ + 'id': actId, + 'handle_index': handleIndex, + 'path': path, + 'to': to, + 'file_num': fileNum, + 'include_hidden': includeHidden, + 'is_remote': isRemote, + }) + ])); +} diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 020bc98aa920..d029de1d2560 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -602,6 +602,7 @@ pub fn session_send_files( file_num: i32, include_hidden: bool, is_remote: bool, + _is_dir: bool, ) { if let Some(session) = sessions::get_session_by_session_id(&session_id) { session.send_files(act_id, path, to, file_num, include_hidden, is_remote); @@ -633,7 +634,7 @@ pub fn session_remove_file( } } -pub fn session_read_dir_recursive( +pub fn session_read_dir_to_remove_recursive( session_id: SessionID, act_id: i32, path: String, From 65683cc3e6da99990d8e4bf5e3d18f6403c3e41e Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Sun, 13 Oct 2024 01:07:47 +0800 Subject: [PATCH 285/541] refact: remove redundant escape (#9634) * refact: remove redundant escape Signed-off-by: fufesou * flutter, early assert Signed-off-by: fufesou --------- Signed-off-by: fufesou --- flutter/lib/common.dart | 4 ++++ flutter/lib/main.dart | 1 + flutter/lib/mobile/pages/remote_page.dart | 8 ++++---- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 881cf3b8b98f..2bbb224cbb10 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -3616,3 +3616,7 @@ List? get subWindowManagerEnableResizeEdges => isWindows SubWindowResizeEdge.topRight, ] : null; + +void earlyAssert() { + assert('\1' == '1'); +} diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index 9342c9e5086c..62574dfe62c7 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -36,6 +36,7 @@ WindowType? kWindowType; late List kBootArgs; Future main(List args) async { + earlyAssert(); WidgetsFlutterBinding.ensureInitialized(); debugPrint("launch args: $args"); diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index 74b56cd45fc6..19f7cc57530d 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -155,9 +155,9 @@ class _RemotePageState extends State { var oldValue = _value; _value = newValue; var i = newValue.length - 1; - for (; i >= 0 && newValue[i] != '\1'; --i) {} + for (; i >= 0 && newValue[i] != '1'; --i) {} var j = oldValue.length - 1; - for (; j >= 0 && oldValue[j] != '\1'; --j) {} + for (; j >= 0 && oldValue[j] != '1'; --j) {} if (i < j) j = i; var subNewValue = newValue.substring(j + 1); var subOldValue = oldValue.substring(j + 1); @@ -206,8 +206,8 @@ class _RemotePageState extends State { _value = newValue; if (oldValue.isNotEmpty && newValue.isNotEmpty && - oldValue[0] == '\1' && - newValue[0] != '\1') { + oldValue[0] == '1' && + newValue[0] != '1') { // clipboard oldValue = ''; } From 9c7f51bc76572c9ab6a2e311cf379e271eec0b84 Mon Sep 17 00:00:00 2001 From: Yevhen Popok Date: Mon, 14 Oct 2024 06:02:44 +0300 Subject: [PATCH 286/541] Update uk.rs (#9638) --- src/lang/uk.rs | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/lang/uk.rs b/src/lang/uk.rs index 05031dcc9306..ac38e3479a58 100644 --- a/src/lang/uk.rs +++ b/src/lang/uk.rs @@ -351,7 +351,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable audio", "Увімкнути аудіо"), ("Unlock Network Settings", "Розблокувати мережеві налаштування"), ("Server", "Сервер"), - ("Direct IP Access", "Прямий IP доступ"), + ("Direct IP Access", "Прямий IP-доступ"), ("Proxy", "Проксі"), ("Apply", "Застосувати"), ("Disconnect all devices?", "Відʼєднати всі прилади?"), @@ -469,12 +469,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("identical_file_tip", "Цей файл ідентичний з тим, що на вузлі"), ("show_monitors_tip", "Показувати монітори на панелі інструментів"), ("View Mode", "Режим перегляду"), - ("login_linux_tip", "Вам необхідно залогуватися у віддалений обліковий запис Linux, щоб увімкнути стільничний сеанс X"), + ("login_linux_tip", "Вам необхідно увійти у віддалений обліковий запис Linux, щоб увімкнути стільничний сеанс X"), ("verify_rustdesk_password_tip", "Перевірте пароль RustDesk"), ("remember_account_tip", "Запамʼятати цей обліковий запис"), - ("os_account_desk_tip", "Цей обліковий запис використовується для входу до віддаленої ОС та вмикання сеансу стільниці в неграфічному режимі"), + ("os_account_desk_tip", "Цей обліковий запис використовується для входу до віддаленої ОС та вмикання сеансу стільниці в режимі без графічного інтерфейсу"), ("OS Account", "Користувач ОС"), - ("another_user_login_title_tip", "Інший користувач вже залогований"), + ("another_user_login_title_tip", "Інший користувач вже в системі"), ("another_user_login_text_tip", "Відʼєднатися"), ("xorg_not_found_title_tip", "Xorg не знайдено"), ("xorg_not_found_text_tip", "Будь ласка, встановіть Xorg"), @@ -506,7 +506,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Outgoing connection", "Вихідне підключення"), ("Exit", "Вийти"), ("Open", "Відкрити"), - ("logout_tip", "Ви впевнені, що хочете вилогуватися?"), + ("logout_tip", "Ви впевнені, що хочете вийти з системи?"), ("Service", "Служба"), ("Start", "Запустити"), ("Stop", "Зупинити"), @@ -563,7 +563,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Plug out all", "Відключити все"), ("True color (4:4:4)", "Справжній колір (4:4:4)"), ("Enable blocking user input", "Блокувати введення для користувача"), - ("id_input_tip", "Ви можете ввести ID, безпосередню IP, або ж домен з портом (<домен>:<порт>).\nЯкщо ви хочете отримати доступ до пристрою на іншому сервері, будь ласка, додайте адресу сервера (@<адреса_сервера>?key=<значення_ключа>), наприклад,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nЯкщо ви хочете отримати доступ до пристрою на публічному сервері, будь ласка, введіть \"@public\", ключ для публічного сервера не потрібен."), + ("id_input_tip", "Ви можете ввести ID, безпосередню IP, або ж домен з портом (<домен>:<порт>).\nЯкщо ви хочете отримати доступ до пристрою на іншому сервері, будь ласка, додайте адресу сервера (@<адреса_сервера>?key=<значення_ключа>), наприклад,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nЯкщо ви хочете отримати доступ до пристрою на публічному сервері, будь ласка, введіть \"@public\", для публічного сервера ключ не потрібен."), ("privacy_mode_impl_mag_tip", "Режим 1"), ("privacy_mode_impl_virtual_display_tip", "Режим 2"), ("Enter privacy mode", "Увійти в режим конфіденційності"), @@ -631,7 +631,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("cancel-bot-confirm-tip", "Ви впевнені, що хочете скасувати Telegram бота?"), ("About RustDesk", "Про Rustdesk"), ("Send clipboard keystrokes", "Надіслати вміст буфера обміну"), - ("network_error_tip", "Будь ласка, перевірте ваше підключення до мережі та натисність \"Повторити\""), + ("network_error_tip", "Будь ласка, перевірте ваше підключення до мережі та натисніть \"Повторити\""), ("Unlock with PIN", "Розблокування PIN-кодом"), ("Requires at least {} characters", "Потрібно щонайменше {} символів"), ("Wrong PIN", "Неправильний PIN-код"), @@ -640,13 +640,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Manage trusted devices", "Керувати довіреними пристроями"), ("Platform", "Платформа"), ("Days remaining", "Залишилося днів"), - ("enable-trusted-devices-tip", "Дозволити довіреним пристроям пропускати двофакторну автентифікацію?"), + ("enable-trusted-devices-tip", "Пропускати двофакторну автентифікацію на довірених пристроях"), ("Parent directory", "Батьківський каталог"), ("Resume", "Продовжити"), - ("Invalid file name", "Неправильне ім'я файлу"), - ("one-way-file-transfer-tip", "На керованій стороні ввімкнено одностороннє передавання файлів."), + ("Invalid file name", "Неправильна назва файлу"), + ("one-way-file-transfer-tip", "На стороні, що керується, увімкнено односторонню передачу файлів."), ("Authentication Required", "Потрібна автентифікація"), ("Authenticate", "Автентифікувати"), - ("web_id_input_tip", ""), + ("web_id_input_tip", "Ви можете ввести ID з того самого серверу, прямий IP-доступ у веб-клієнті не підтримується.\nЯкщо ви хочете отримати доступ до пристрою на іншому сервері, будь ласка, додайте адресу сервера (@<адреса_сервера>?key=<значення_ключа>), наприклад,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nЯкщо ви хочете отримати доступ до пристрою на публічному сервері, будь ласка, введіть \"@public\", для публічного сервера ключ не потрібен."), ].iter().cloned().collect(); } From 6cdbcfc0821cc3b073fa45de29b245b18166ad71 Mon Sep 17 00:00:00 2001 From: Yevhen Popok Date: Mon, 14 Oct 2024 06:02:57 +0300 Subject: [PATCH 287/541] Update README-UA.md (#9639) --- docs/README-UA.md | 36 +++++++++++++++++++----------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/docs/README-UA.md b/docs/README-UA.md index c4d2e6f9f390..8f226914d709 100644 --- a/docs/README-UA.md +++ b/docs/README-UA.md @@ -1,20 +1,18 @@

    RustDesk - Ваша віддалена стільниця
    - Сервери • + СервериЗбиранняDockerСтруктура • - Знімки
    - [English] | [česky] | [中文] | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt] | [Dansk] | [Ελληνικά] | [Türkçe]
    - Нам потрібна ваша допомога для перекладу цього README, інтерфейсу та документації RustDesk на вашу рідну мову + Знімки екрана
    + [English] | [česky] | [中文] | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt] | [Dansk] | [Ελληνικά] | [Türkçe]
    + Нам потрібна ваша допомога для перекладу цього README, інтерфейсу та документації 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) -[![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). ![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png) @@ -61,19 +59,19 @@ RustDesk вітає внесок кожного. Ознайомтеся з [CONT ```sh sudo apt install -y zip g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev \ libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake make \ - libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev + libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libpam0g-dev ``` ### openSUSE Tumbleweed ```sh -sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel +sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel pam-devel ``` ### Fedora 28 (CentOS 8) ```sh -sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel +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) @@ -158,18 +156,22 @@ target/release/rustdesk - **[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/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: сервіси аудіо/буфера обміну/вводу/відео та мережевих підключень -- **[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/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/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)**: JavaScript для Flutter веб клієнту +- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: JavaScript для веб клієнта на Flutter + +## Знімки екрана + +![Менеджер зʼєднань](https://github.com/rustdesk/rustdesk/assets/28412477/db82d4e7-c4bc-4823-8e6f-6af7eadf7651) -## Знімки +![Підключення до ПК з Windows](https://github.com/rustdesk/rustdesk/assets/28412477/9baa91e9-3362-4d06-aa1a-7518edcbd7ea) -![image](https://user-images.githubusercontent.com/71636191/113112362-ae4deb80-923b-11eb-957d-ff88daad4f06.png) +![Передача файлів](https://github.com/rustdesk/rustdesk/assets/28412477/39511ad3-aa9a-4f8c-8947-1cce286a46ad) -![image](https://user-images.githubusercontent.com/71636191/113112619-f705a480-923b-11eb-911d-97e984ef52b6.png) +![Тунелювання TCP](https://github.com/rustdesk/rustdesk/assets/28412477/78e8708f-e87e-4570-8373-1360033ea6c5) -![image](https://user-images.githubusercontent.com/71636191/113112857-3fbd5d80-923c-11eb-9836-768325faf906.png) +## [Публічні сервери](#публічні-сервери) -![image](https://user-images.githubusercontent.com/71636191/135385039-38fdbd72-379a-422d-b97f-33df71fb1cec.png) +RustDesk підтримується безкоштовним європейським сервером, любʼязно наданим [Codext GmbH](https://codext.link/rustdesk?utm_source=github) From af610b2408b7de0878c65b63622a6d695cef5e80 Mon Sep 17 00:00:00 2001 From: 21pages Date: Mon, 14 Oct 2024 11:15:52 +0800 Subject: [PATCH 288/541] web (#9640) resolution, image quality, tab name, last remote id Signed-off-by: 21pages --- flutter/lib/common/widgets/peer_card.dart | 8 +++--- flutter/lib/main.dart | 4 ++- flutter/lib/mobile/pages/connection_page.dart | 8 +++++- flutter/lib/mobile/pages/home_page.dart | 2 +- flutter/lib/web/bridge.dart | 28 +++++++++---------- 5 files changed, 28 insertions(+), 22 deletions(-) diff --git a/flutter/lib/common/widgets/peer_card.dart b/flutter/lib/common/widgets/peer_card.dart index 690c829775f7..0a15eb45b880 100644 --- a/flutter/lib/common/widgets/peer_card.dart +++ b/flutter/lib/common/widgets/peer_card.dart @@ -938,7 +938,7 @@ class FavoritePeerCard extends BasePeerCard { BuildContext context) async { final List> menuItems = [ _connectAction(context), - if (!isWeb) _transferFileAction(context), + _transferFileAction(context), ]; if (isDesktop && peer.platform != kPeerPlatformAndroid) { menuItems.add(_tcpTunnelingAction(context)); @@ -991,7 +991,7 @@ class DiscoveredPeerCard extends BasePeerCard { BuildContext context) async { final List> menuItems = [ _connectAction(context), - if (!isWeb) _transferFileAction(context), + _transferFileAction(context), ]; final List favs = (await bind.mainGetFav()).toList(); @@ -1044,7 +1044,7 @@ class AddressBookPeerCard extends BasePeerCard { BuildContext context) async { final List> menuItems = [ _connectAction(context), - if (!isWeb) _transferFileAction(context), + _transferFileAction(context), ]; if (isDesktop && peer.platform != kPeerPlatformAndroid) { menuItems.add(_tcpTunnelingAction(context)); @@ -1176,7 +1176,7 @@ class MyGroupPeerCard extends BasePeerCard { BuildContext context) async { final List> menuItems = [ _connectAction(context), - if (!isWeb) _transferFileAction(context), + _transferFileAction(context), ]; if (isDesktop && peer.platform != kPeerPlatformAndroid) { menuItems.add(_tcpTunnelingAction(context)); diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index 62574dfe62c7..18578f81b56f 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -445,7 +445,9 @@ class _AppState extends State with WidgetsBindingObserver { child: GetMaterialApp( navigatorKey: globalKey, debugShowCheckedModeBanner: false, - title: 'RustDesk', + title: isWeb + ? '${bind.mainGetAppNameSync()} Web Client V2 (Preview)' + : bind.mainGetAppNameSync(), theme: MyTheme.lightTheme, darkTheme: MyTheme.darkTheme, themeMode: MyTheme.currentThemeMode(), diff --git a/flutter/lib/mobile/pages/connection_page.dart b/flutter/lib/mobile/pages/connection_page.dart index e71cc1c56470..89b71c177c91 100644 --- a/flutter/lib/mobile/pages/connection_page.dart +++ b/flutter/lib/mobile/pages/connection_page.dart @@ -212,6 +212,8 @@ class _ConnectionPageState extends State { FocusNode fieldFocusNode, VoidCallback onFieldSubmitted) { fieldTextEditingController.text = _idController.text; + Get.put( + fieldTextEditingController); fieldFocusNode.addListener(() async { _idEmpty.value = fieldTextEditingController.text.isEmpty; @@ -352,7 +354,8 @@ class _ConnectionPageState extends State { ); final child = Column(children: [ if (isWebDesktop) - getConnectionPageTitle(context, true).marginOnly(bottom: 10, top: 15, left: 12), + getConnectionPageTitle(context, true) + .marginOnly(bottom: 10, top: 15, left: 12), w ]); return Align( @@ -367,6 +370,9 @@ class _ConnectionPageState extends State { if (Get.isRegistered()) { Get.delete(); } + if (Get.isRegistered()) { + Get.delete(); + } if (!bind.isCustomClient()) { platformFFI.unregisterEventHandler( kCheckSoftwareUpdateFinish, kCheckSoftwareUpdateFinish); diff --git a/flutter/lib/mobile/pages/home_page.dart b/flutter/lib/mobile/pages/home_page.dart index e329acdfe17e..bad569afeabe 100644 --- a/flutter/lib/mobile/pages/home_page.dart +++ b/flutter/lib/mobile/pages/home_page.dart @@ -165,7 +165,7 @@ class WebHomePage extends StatelessWidget { // backgroundColor: MyTheme.grayBg, appBar: AppBar( centerTitle: true, - title: Text(bind.mainGetAppNameSync()), + title: Text("${bind.mainGetAppNameSync()} (Preview)"), actions: connectionPage.appBarActions, ), body: connectionPage, diff --git a/flutter/lib/web/bridge.dart b/flutter/lib/web/bridge.dart index 0362490cdf10..ec1fea86e78e 100644 --- a/flutter/lib/web/bridge.dart +++ b/flutter/lib/web/bridge.dart @@ -284,16 +284,14 @@ class RustdeskImpl { Future sessionGetImageQuality( {required UuidValue sessionId, dynamic hint}) { - return Future(() => js.context - .callMethod('getByName', ['option:session', 'image_quality'])); + return Future(() => js.context.callMethod('getByName', ['image_quality'])); } Future sessionSetImageQuality( {required UuidValue sessionId, required String value, dynamic hint}) { - return Future(() => js.context.callMethod('setByName', [ - 'option:session', - jsonEncode({'name': 'image_quality', 'value': value}) - ])); + print('set image quality: $value'); + return Future( + () => js.context.callMethod('setByName', ['image_quality', value])); } Future sessionGetKeyboardMode( @@ -374,17 +372,15 @@ class RustdeskImpl { Future sessionSetCustomImageQuality( {required UuidValue sessionId, required int value, dynamic hint}) { return Future(() => js.context.callMethod('setByName', [ - 'option:session', - jsonEncode({'name': 'custom_image_quality', 'value': value}) + 'custom_image_quality', + value, ])); } Future sessionSetCustomFps( {required UuidValue sessionId, required int fps, dynamic hint}) { - return Future(() => js.context.callMethod('setByName', [ - 'option:session', - jsonEncode({'name': 'custom_fps', 'value': fps}) - ])); + return Future( + () => js.context.callMethod('setByName', ['custom-fps', fps])); } Future sessionLockScreen({required UuidValue sessionId, dynamic hint}) { @@ -694,7 +690,10 @@ class RustdeskImpl { required int height, dynamic hint}) { // note: restore on disconnected - throw UnimplementedError("sessionChangeResolution"); + return Future(() => js.context.callMethod('setByName', [ + 'change_resolution', + jsonEncode({'display': display, 'width': width, 'height': height}) + ])); } Future sessionSetSize( @@ -1119,8 +1118,7 @@ class RustdeskImpl { } Future mainGetLastRemoteId({dynamic hint}) { - return Future( - () => js.context.callMethod('getByName', ['option', 'last_remote_id'])); + return Future(() => mainGetLocalOption(key: 'last_remote_id')); } Future mainGetSoftwareUpdateUrl({dynamic hint}) { From 498b8ba3d616dc318e9cbed403ff722c7eac3da0 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Mon, 14 Oct 2024 11:46:28 +0800 Subject: [PATCH 289/541] fix: wayland CapsLock (#9641) * fix: wayland CapsLock Signed-off-by: fufesou * unformat for less changes Signed-off-by: fufesou * refact: input, LockModes, do not check evt.down Remove `evt.down`(revert the change) to avoid potential bugs. Signed-off-by: fufesou --------- Signed-off-by: fufesou --- src/server/input_service.rs | 36 +++++++++++++++++++++++++++++++++--- src/server/uinput.rs | 4 ++-- 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/src/server/input_service.rs b/src/server/input_service.rs index fd4291cc8f61..afab184abe99 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -175,6 +175,22 @@ impl LockModesHandler { } } + #[cfg(target_os = "linux")] + fn sleep_to_ensure_locked(v: bool, k: enigo::Key, en: &mut Enigo) { + if wayland_use_uinput() { + // Sleep at most 500ms to ensure the lock state is applied. + for _ in 0..50 { + std::thread::sleep(std::time::Duration::from_millis(10)); + if en.get_key_state(k) == v { + break; + } + } + } else if wayland_use_rdp_input() { + // We can't call `en.get_key_state(k)` because there's no api for this. + std::thread::sleep(std::time::Duration::from_millis(50)); + } + } + #[cfg(any(target_os = "windows", target_os = "linux"))] fn new(key_event: &KeyEvent, is_numpad_key: bool) -> Self { let mut en = ENIGO.lock().unwrap(); @@ -183,12 +199,15 @@ impl LockModesHandler { let caps_lock_changed = event_caps_enabled != local_caps_enabled; if caps_lock_changed { en.key_click(enigo::Key::CapsLock); + #[cfg(target_os = "linux")] + Self::sleep_to_ensure_locked(event_caps_enabled, enigo::Key::CapsLock, &mut en); } let mut num_lock_changed = false; + let mut event_num_enabled = false; if is_numpad_key { let local_num_enabled = en.get_key_state(enigo::Key::NumLock); - let event_num_enabled = Self::is_modifier_enabled(key_event, ControlKey::NumLock); + event_num_enabled = Self::is_modifier_enabled(key_event, ControlKey::NumLock); num_lock_changed = event_num_enabled != local_num_enabled; } else if is_legacy_mode(key_event) { #[cfg(target_os = "windows")] @@ -199,6 +218,8 @@ impl LockModesHandler { } if num_lock_changed { en.key_click(enigo::Key::NumLock); + #[cfg(target_os = "linux")] + Self::sleep_to_ensure_locked(event_num_enabled, enigo::Key::NumLock, &mut en); } Self { @@ -236,6 +257,14 @@ impl LockModesHandler { #[cfg(any(target_os = "windows", target_os = "linux"))] impl Drop for LockModesHandler { fn drop(&mut self) { + // Do not change led state if is Wayland uinput. + // Because there must be a delay to ensure the lock state is applied on Wayland uinput, + // which may affect the user experience. + #[cfg(target_os = "linux")] + if wayland_use_uinput() { + return; + } + let mut en = ENIGO.lock().unwrap(); if self.caps_lock_changed { en.key_click(enigo::Key::CapsLock); @@ -1633,12 +1662,13 @@ pub fn handle_key_(evt: &KeyEvent) { let is_numpad_key = false; #[cfg(any(target_os = "windows", target_os = "linux"))] let is_numpad_key = crate::keyboard::is_numpad_rdev_key(&key); - _lock_mode_handler = Some(LockModesHandler::new_handler(evt, is_numpad_key)); + _lock_mode_handler = + Some(LockModesHandler::new_handler(evt, is_numpad_key)); } } } _ => {} - }; + } match evt.mode.enum_value() { Ok(KeyboardMode::Map) => { diff --git a/src/server/uinput.rs b/src/server/uinput.rs index 942f3753a76d..60c647862ad7 100644 --- a/src/server/uinput.rs +++ b/src/server/uinput.rs @@ -431,8 +431,8 @@ pub mod service { allow_err!(keyboard.emit(&[down_event])); } DataKeyboard::KeyUp(enigo::Key::Raw(code)) => { - let down_event = InputEvent::new(EventType::KEY, *code - 8, 0); - allow_err!(keyboard.emit(&[down_event])); + let up_event = InputEvent::new(EventType::KEY, *code - 8, 0); + allow_err!(keyboard.emit(&[up_event])); } DataKeyboard::KeyDown(key) => { if let Ok((k, is_shift)) = map_key(key) { From ce924cc0d3232c451a66bbb2fcc247b0c29cb2b2 Mon Sep 17 00:00:00 2001 From: 21pages Date: Mon, 14 Oct 2024 15:46:21 +0800 Subject: [PATCH 290/541] combine upload files/folder button (#9643) * combine upload files/folder button Signed-off-by: 21pages * web compress cache Signed-off-by: 21pages --------- Signed-off-by: 21pages --- .gitignore | 1 + .../lib/desktop/pages/file_manager_page.dart | 99 +++++++++++-------- flutter/lib/web/bridge.dart | 14 ++- src/lang/ar.rs | 3 + src/lang/be.rs | 3 + src/lang/bg.rs | 3 + src/lang/ca.rs | 3 + src/lang/cn.rs | 3 + src/lang/cs.rs | 3 + src/lang/da.rs | 3 + src/lang/de.rs | 3 + src/lang/el.rs | 3 + src/lang/eo.rs | 3 + src/lang/es.rs | 3 + src/lang/et.rs | 3 + src/lang/eu.rs | 3 + src/lang/fa.rs | 3 + src/lang/fr.rs | 3 + src/lang/he.rs | 3 + src/lang/hr.rs | 3 + src/lang/hu.rs | 3 + src/lang/id.rs | 3 + src/lang/it.rs | 3 + src/lang/ja.rs | 3 + src/lang/ko.rs | 3 + src/lang/kz.rs | 3 + src/lang/lt.rs | 3 + src/lang/lv.rs | 3 + src/lang/nb.rs | 3 + src/lang/nl.rs | 3 + src/lang/pl.rs | 3 + src/lang/pt_PT.rs | 3 + src/lang/ptbr.rs | 3 + src/lang/ro.rs | 3 + src/lang/ru.rs | 3 + src/lang/sk.rs | 3 + src/lang/sl.rs | 3 + src/lang/sq.rs | 3 + src/lang/sr.rs | 3 + src/lang/sv.rs | 3 + src/lang/template.rs | 3 + src/lang/th.rs | 3 + src/lang/tr.rs | 3 + src/lang/tw.rs | 3 + src/lang/uk.rs | 3 + src/lang/vn.rs | 3 + 46 files changed, 201 insertions(+), 42 deletions(-) diff --git a/.gitignore b/.gitignore index 30e1aafe2f10..b4ea62660468 100644 --- a/.gitignore +++ b/.gitignore @@ -54,3 +54,4 @@ examples/**/target/ vcpkg_installed flutter/lib/generated_plugin_registrant.dart libsciter.dylib +flutter/web/ \ No newline at end of file diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index 5557783b5c76..ba1a37fb1541 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -490,6 +490,9 @@ class _FileManagerViewState extends State { } Widget headTools() { + var uploadButtonTapPosition = RelativeRect.fill; + RxBool isUploadFolder = + (bind.mainGetLocalOption(key: 'upload-folder-button') == 'Y').obs; return Container( child: Column( children: [ @@ -814,48 +817,64 @@ class _FileManagerViewState extends State { ), if (isWeb) Obx(() => ElevatedButton.icon( - style: ButtonStyle( - padding: MaterialStateProperty.all( - isLocal - ? EdgeInsets.only(left: 10) - : EdgeInsets.only(right: 10)), - backgroundColor: MaterialStateProperty.all( - selectedItems.items.isEmpty - ? MyTheme.accent80 - : MyTheme.accent, - ), - ), - onPressed: () => {webselectFiles(is_folder: true)}, - icon: Offstage(), - label: Text( - translate('Upload folder'), - textAlign: TextAlign.right, - style: TextStyle( - color: Colors.white, - ), - ))).marginOnly(left: 16), - if (isWeb) - Obx(() => ElevatedButton.icon( - style: ButtonStyle( - padding: MaterialStateProperty.all( - isLocal - ? EdgeInsets.only(left: 10) - : EdgeInsets.only(right: 10)), - backgroundColor: MaterialStateProperty.all( - selectedItems.items.isEmpty - ? MyTheme.accent80 - : MyTheme.accent, + style: ButtonStyle( + padding: MaterialStateProperty.all( + isLocal + ? EdgeInsets.only(left: 10) + : EdgeInsets.only(right: 10)), + backgroundColor: MaterialStateProperty.all( + selectedItems.items.isEmpty + ? MyTheme.accent80 + : MyTheme.accent, + ), ), - ), - onPressed: () => {webselectFiles(is_folder: false)}, - icon: Offstage(), - label: Text( - translate('Upload files'), - textAlign: TextAlign.right, - style: TextStyle( - color: Colors.white, + 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( + context: context, + position: uploadButtonTapPosition, + items: [ + PopupMenuItem( + value: false, + child: Text(translate('Upload files')), + ), + PopupMenuItem( + 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), ), - ))).marginOnly(left: 16), + 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( style: ButtonStyle( padding: MaterialStateProperty.all( diff --git a/flutter/lib/web/bridge.dart b/flutter/lib/web/bridge.dart index ec1fea86e78e..9238d054a6d5 100644 --- a/flutter/lib/web/bridge.dart +++ b/flutter/lib/web/bridge.dart @@ -1209,7 +1209,12 @@ class RustdeskImpl { } Future mainLoadAb({dynamic hint}) { - return Future(() => js.context.callMethod('getByName', ['load_ab'])); + Completer completer = Completer(); + js.context["onLoadAbFinished"] = (String s) { + completer.complete(s); + }; + js.context.callMethod('setByName', ['load_ab']); + return completer.future; } Future mainSaveGroup({required String json, dynamic hint}) { @@ -1222,7 +1227,12 @@ class RustdeskImpl { } Future mainLoadGroup({dynamic hint}) { - return Future(() => js.context.callMethod('getByName', ['load_group'])); + Completer completer = Completer(); + js.context["onLoadGroupFinished"] = (String s) { + completer.complete(s); + }; + js.context.callMethod('setByName', ['load_group']); + return completer.future; } Future sessionSendPointer( diff --git a/src/lang/ar.rs b/src/lang/ar.rs index 0990614e2f0c..879414cd613f 100644 --- a/src/lang/ar.rs +++ b/src/lang/ar.rs @@ -648,5 +648,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Authentication Required", ""), ("Authenticate", ""), ("web_id_input_tip", ""), + ("Download", ""), + ("Upload folder", ""), + ("Upload files", ""), ].iter().cloned().collect(); } diff --git a/src/lang/be.rs b/src/lang/be.rs index 2b9167a9cc26..501438fd0e4f 100644 --- a/src/lang/be.rs +++ b/src/lang/be.rs @@ -648,5 +648,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Authentication Required", ""), ("Authenticate", ""), ("web_id_input_tip", ""), + ("Download", ""), + ("Upload folder", ""), + ("Upload files", ""), ].iter().cloned().collect(); } diff --git a/src/lang/bg.rs b/src/lang/bg.rs index 7e7e7a05c213..cf972ee766ca 100644 --- a/src/lang/bg.rs +++ b/src/lang/bg.rs @@ -648,5 +648,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Authentication Required", ""), ("Authenticate", ""), ("web_id_input_tip", ""), + ("Download", ""), + ("Upload folder", ""), + ("Upload files", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ca.rs b/src/lang/ca.rs index cd262589da79..4001c1e6e69b 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -648,5 +648,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Authentication Required", "Autenticació requerida"), ("Authenticate", "Autentica"), ("web_id_input_tip", "Podeu inserir el número ID al propi servidor; l'accés directe per IP no és compatible amb el client web.\nSi voleu accedir a un dispositiu d'un altre servidor, afegiu l'adreça del servidor, com ara @?key= (p. ex.\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nSi voleu accedir a un dispositiu en un servidor públic, no cal que inseriu la clau pública «@» per al servidor públic."), + ("Download", ""), + ("Upload folder", ""), + ("Upload files", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 7cc616e14270..17b3ba159af5 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -648,5 +648,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Authentication Required", "需要身份验证"), ("Authenticate", "认证"), ("web_id_input_tip", "可以输入同一个服务器内的 ID,web 客户端不支持直接 IP 访问。\n要访问另一台服务器上的设备,请附加服务器地址(@<服务器地址>?key=<密钥>)。比如,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=。\n要访问公共服务器上的设备,请输入 \"@public\",无需密钥。"), + ("Download", "下载"), + ("Upload folder", "上传文件夹"), + ("Upload files", "上传文件"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index c93dd37b7f91..8404ee3e41da 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -648,5 +648,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Authentication Required", ""), ("Authenticate", ""), ("web_id_input_tip", ""), + ("Download", ""), + ("Upload folder", ""), + ("Upload files", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index f59accfe1c79..c59f3efc9c7a 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -648,5 +648,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Authentication Required", ""), ("Authenticate", ""), ("web_id_input_tip", ""), + ("Download", ""), + ("Upload folder", ""), + ("Upload files", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index 4a7bb08dbf76..11a0dbb3275e 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -648,5 +648,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Authentication Required", "Authentifizierung erforderlich"), ("Authenticate", "Authentifizieren"), ("web_id_input_tip", "Sie können eine ID auf demselben Server eingeben, direkter IP-Zugriff wird im Web-Client nicht unterstützt.\nWenn Sie auf ein Gerät auf einem anderen Server zugreifen wollen, fügen Sie bitte die Serveradresse (@?key=) hinzu, zum Beispiel\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nWenn Sie auf ein Gerät auf einem öffentlichen Server zugreifen wollen, geben Sie bitte \"@public\" ein. Der Schlüssel wird für öffentliche Server nicht benötigt."), + ("Download", ""), + ("Upload folder", ""), + ("Upload files", ""), ].iter().cloned().collect(); } diff --git a/src/lang/el.rs b/src/lang/el.rs index 779e71d23ea2..eaa31c55fadb 100644 --- a/src/lang/el.rs +++ b/src/lang/el.rs @@ -648,5 +648,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Authentication Required", ""), ("Authenticate", ""), ("web_id_input_tip", ""), + ("Download", ""), + ("Upload folder", ""), + ("Upload files", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index ba369e4ec110..e7daf0ff7957 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -648,5 +648,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Authentication Required", ""), ("Authenticate", ""), ("web_id_input_tip", ""), + ("Download", ""), + ("Upload folder", ""), + ("Upload files", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index 369cd5b95158..bd7e4df153d5 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -648,5 +648,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Authentication Required", "Se requiere autenticación"), ("Authenticate", "Autenticar"), ("web_id_input_tip", ""), + ("Download", ""), + ("Upload folder", ""), + ("Upload files", ""), ].iter().cloned().collect(); } diff --git a/src/lang/et.rs b/src/lang/et.rs index db7970f16e45..131e3b2156b4 100644 --- a/src/lang/et.rs +++ b/src/lang/et.rs @@ -648,5 +648,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Authentication Required", ""), ("Authenticate", ""), ("web_id_input_tip", ""), + ("Download", ""), + ("Upload folder", ""), + ("Upload files", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eu.rs b/src/lang/eu.rs index 90976236e574..267431efd1d2 100644 --- a/src/lang/eu.rs +++ b/src/lang/eu.rs @@ -648,5 +648,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Authentication Required", ""), ("Authenticate", ""), ("web_id_input_tip", ""), + ("Download", ""), + ("Upload folder", ""), + ("Upload files", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index 71bea4e78929..c070f18e20dd 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -648,5 +648,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Authentication Required", ""), ("Authenticate", ""), ("web_id_input_tip", ""), + ("Download", ""), + ("Upload folder", ""), + ("Upload files", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 97f8d651406f..2fa41f3fe6e6 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -648,5 +648,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Authentication Required", ""), ("Authenticate", ""), ("web_id_input_tip", ""), + ("Download", ""), + ("Upload folder", ""), + ("Upload files", ""), ].iter().cloned().collect(); } diff --git a/src/lang/he.rs b/src/lang/he.rs index 526873de3d2d..5d5e4637e810 100644 --- a/src/lang/he.rs +++ b/src/lang/he.rs @@ -648,5 +648,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Authentication Required", ""), ("Authenticate", ""), ("web_id_input_tip", ""), + ("Download", ""), + ("Upload folder", ""), + ("Upload files", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hr.rs b/src/lang/hr.rs index e210e7566b6d..378f0e90977c 100644 --- a/src/lang/hr.rs +++ b/src/lang/hr.rs @@ -648,5 +648,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Authentication Required", ""), ("Authenticate", ""), ("web_id_input_tip", ""), + ("Download", ""), + ("Upload folder", ""), + ("Upload files", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index ccb2be1cb02e..8aaaa3a13a7d 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -648,5 +648,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Authentication Required", ""), ("Authenticate", ""), ("web_id_input_tip", ""), + ("Download", ""), + ("Upload folder", ""), + ("Upload files", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index efedb159778c..44703afc6669 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -648,5 +648,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Authentication Required", "Diperlukan autentikasi"), ("Authenticate", "Autentikasi"), ("web_id_input_tip", "Kamu bisa memasukkan ID pada server yang sama, akses IP langsung tidak didukung di klien web.\nJika Anda ingin mengakses perangkat di server lain, silakan tambahkan alamat server (@?key=), contohnya:\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nUntuk mengakses perangkat di server publik, cukup masukkan \"@public\", tanpa kunci/key."), + ("Download", ""), + ("Upload folder", ""), + ("Upload files", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index 68a24265999e..3aa0966d48ae 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -648,5 +648,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Authentication Required", "Richiesta autenticazione"), ("Authenticate", "Autentica"), ("web_id_input_tip", "È possibile inserire un ID nello stesso server, nel client web non è supportato l'accesso con IP diretto.\nSe vuoi accedere ad un dispositivo in un altro server, aggiungi l'indirizzo del server (@?key=), ad esempio,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nSe vuoi accedere ad un dispositivo in un server pubblico, inserisci \"@public\", la chiave non è necessaria per il server pubblico."), + ("Download", ""), + ("Upload folder", ""), + ("Upload files", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index eca28da863ea..41fcca10ce24 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -648,5 +648,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Authentication Required", ""), ("Authenticate", ""), ("web_id_input_tip", ""), + ("Download", ""), + ("Upload folder", ""), + ("Upload files", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index a3c24e98cd75..5c9660560d0d 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -648,5 +648,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Authentication Required", "인증 필요함"), ("Authenticate", "인증"), ("web_id_input_tip", ""), + ("Download", ""), + ("Upload folder", ""), + ("Upload files", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index 404e9b511af8..3b5aba5e4cd0 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -648,5 +648,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Authentication Required", ""), ("Authenticate", ""), ("web_id_input_tip", ""), + ("Download", ""), + ("Upload folder", ""), + ("Upload files", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lt.rs b/src/lang/lt.rs index a93cef88c3db..c6915aab3171 100644 --- a/src/lang/lt.rs +++ b/src/lang/lt.rs @@ -648,5 +648,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Authentication Required", ""), ("Authenticate", ""), ("web_id_input_tip", ""), + ("Download", ""), + ("Upload folder", ""), + ("Upload files", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lv.rs b/src/lang/lv.rs index 0cddcb951b26..4a24a38a0cb1 100644 --- a/src/lang/lv.rs +++ b/src/lang/lv.rs @@ -648,5 +648,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Authentication Required", "Nepieciešama autentifikācija"), ("Authenticate", "Autentificēt"), ("web_id_input_tip", ""), + ("Download", ""), + ("Upload folder", ""), + ("Upload files", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nb.rs b/src/lang/nb.rs index 7f08f83d10c4..a5b501aa46cb 100644 --- a/src/lang/nb.rs +++ b/src/lang/nb.rs @@ -648,5 +648,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Authentication Required", ""), ("Authenticate", ""), ("web_id_input_tip", ""), + ("Download", ""), + ("Upload folder", ""), + ("Upload files", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nl.rs b/src/lang/nl.rs index 1b8a96e540d3..6263cccb8e35 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -648,5 +648,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Authentication Required", "Verificatie vereist"), ("Authenticate", "Verificatie"), ("web_id_input_tip", "Je kunt een ID invoeren op dezelfde server, directe IP-toegang wordt niet ondersteund in de webclient.\nAls je toegang wilt tot een apparaat op een andere server, voeg je het serveradres toe (@?key=), bijvoorbeeld,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nAls je toegang wilt krijgen tot een apparaat op een publieke server, voer dan \"@public\" in, sleutel is niet nodig voor de publieke server."), + ("Download", ""), + ("Upload folder", ""), + ("Upload files", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index a7216e780415..8479949b3176 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -648,5 +648,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Authentication Required", ""), ("Authenticate", ""), ("web_id_input_tip", ""), + ("Download", ""), + ("Upload folder", ""), + ("Upload files", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index 994d5cc63db1..a30692c42e1c 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -648,5 +648,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Authentication Required", ""), ("Authenticate", ""), ("web_id_input_tip", ""), + ("Download", ""), + ("Upload folder", ""), + ("Upload files", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index ba6fe051b81b..45dc2259ba84 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -648,5 +648,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Authentication Required", ""), ("Authenticate", ""), ("web_id_input_tip", ""), + ("Download", ""), + ("Upload folder", ""), + ("Upload files", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ro.rs b/src/lang/ro.rs index 429c2534ab7b..50687686544d 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -648,5 +648,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Authentication Required", ""), ("Authenticate", ""), ("web_id_input_tip", ""), + ("Download", ""), + ("Upload folder", ""), + ("Upload files", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 3ab54b42a083..4bc1516d1159 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -648,5 +648,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Authentication Required", "Требуется аутентификация"), ("Authenticate", "Аутентификация"), ("web_id_input_tip", "Можно ввести ID на том же сервере, прямой доступ по IP в веб-клиенте не поддерживается.\nЕсли вы хотите получить доступ к устройству на другом сервере, добавьте адрес сервера (@<адрес_сервера>?key=<ключ>), например,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nЕсли вы хотите получить доступ к устройству на публичном сервере, введите \"@public\", для публичного сервера ключ не нужен."), + ("Download", ""), + ("Upload folder", ""), + ("Upload files", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 1e5a51f9f551..dabec3ed3dec 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -648,5 +648,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Authentication Required", ""), ("Authenticate", ""), ("web_id_input_tip", ""), + ("Download", ""), + ("Upload folder", ""), + ("Upload files", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sl.rs b/src/lang/sl.rs index 7d18ef829823..f834687c78fb 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -648,5 +648,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Authentication Required", "Potrebno je preverjanje pristnosti"), ("Authenticate", "Preverjanje pristnosti"), ("web_id_input_tip", "Vnesete lahko ID iz istega strežnika, neposredni dostop preko IP naslova v spletnem odjemalcu ni podprt.\nČe želite dostopati do naprave na drugem strežniku, pripnite naslov strežnika (@?key=), npr. 9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nČe želite dostopati do naprave na javnem strežniku, vnesite »@public«; ključ za javni strežnik ni potreben."), + ("Download", ""), + ("Upload folder", ""), + ("Upload files", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sq.rs b/src/lang/sq.rs index b3e85a351e1d..acc5a627f899 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -648,5 +648,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Authentication Required", ""), ("Authenticate", ""), ("web_id_input_tip", ""), + ("Download", ""), + ("Upload folder", ""), + ("Upload files", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sr.rs b/src/lang/sr.rs index ee84a68e1b81..0ade32f819db 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -648,5 +648,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Authentication Required", ""), ("Authenticate", ""), ("web_id_input_tip", ""), + ("Download", ""), + ("Upload folder", ""), + ("Upload files", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sv.rs b/src/lang/sv.rs index 9ba9bb5362ad..5dd93ea4ea2c 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -648,5 +648,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Authentication Required", ""), ("Authenticate", ""), ("web_id_input_tip", ""), + ("Download", ""), + ("Upload folder", ""), + ("Upload files", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index e62119843480..eb62d5621cf9 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -648,5 +648,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Authentication Required", ""), ("Authenticate", ""), ("web_id_input_tip", ""), + ("Download", ""), + ("Upload folder", ""), + ("Upload files", ""), ].iter().cloned().collect(); } diff --git a/src/lang/th.rs b/src/lang/th.rs index a2f8bc9103ce..deb7f974191f 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -648,5 +648,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Authentication Required", ""), ("Authenticate", ""), ("web_id_input_tip", ""), + ("Download", ""), + ("Upload folder", ""), + ("Upload files", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index 78b09dca47e9..656feb0ef079 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -648,5 +648,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Authentication Required", ""), ("Authenticate", ""), ("web_id_input_tip", ""), + ("Download", ""), + ("Upload folder", ""), + ("Upload files", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 5bb26acce158..776151ae7c46 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -648,5 +648,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Authentication Required", "需要身分驗證"), ("Authenticate", "認證"), ("web_id_input_tip", ""), + ("Download", ""), + ("Upload folder", ""), + ("Upload files", ""), ].iter().cloned().collect(); } diff --git a/src/lang/uk.rs b/src/lang/uk.rs index ac38e3479a58..35ffa1c290da 100644 --- a/src/lang/uk.rs +++ b/src/lang/uk.rs @@ -648,5 +648,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Authentication Required", "Потрібна автентифікація"), ("Authenticate", "Автентифікувати"), ("web_id_input_tip", "Ви можете ввести ID з того самого серверу, прямий IP-доступ у веб-клієнті не підтримується.\nЯкщо ви хочете отримати доступ до пристрою на іншому сервері, будь ласка, додайте адресу сервера (@<адреса_сервера>?key=<значення_ключа>), наприклад,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nЯкщо ви хочете отримати доступ до пристрою на публічному сервері, будь ласка, введіть \"@public\", для публічного сервера ключ не потрібен."), + ("Download", ""), + ("Upload folder", ""), + ("Upload files", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index bc6311fcd9ce..d07225ba0a4d 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -648,5 +648,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Authentication Required", ""), ("Authenticate", ""), ("web_id_input_tip", ""), + ("Download", ""), + ("Upload folder", ""), + ("Upload files", ""), ].iter().cloned().collect(); } From cdd58e77ebbf0718b0cdbef8e5314b7cde0bdc4a Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Mon, 14 Oct 2024 15:48:56 +0800 Subject: [PATCH 291/541] fix: flickers child screen when resizing window (#9645) Signed-off-by: fufesou --- flutter/lib/utils/multi_window_manager.dart | 3 +++ flutter/pubspec.lock | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/flutter/lib/utils/multi_window_manager.dart b/flutter/lib/utils/multi_window_manager.dart index 191152c8625a..fa35b4fe9717 100644 --- a/flutter/lib/utils/multi_window_manager.dart +++ b/flutter/lib/utils/multi_window_manager.dart @@ -124,6 +124,9 @@ class RustDeskMultiWindowManager { bool withScreenRect, ) async { final windowController = await DesktopMultiWindow.createWindow(msg); + if (isWindows) { + windowController.setInitBackgroundColor(Colors.black); + } final windowId = windowController.windowId; if (!withScreenRect) { windowController diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index 6210b5399c81..6551bbb37bfb 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -335,7 +335,7 @@ packages: description: path: "." ref: HEAD - resolved-ref: "0842f44d8644911f65a6b78be22474af0f8a9349" + resolved-ref: "519350f1f40746798299e94786197d058353bac9" url: "https://github.com/rustdesk-org/rustdesk_desktop_multi_window" source: git version: "0.1.0" From ae1c1a56e6292558a3de4524fe974073ddd8547d Mon Sep 17 00:00:00 2001 From: 21pages Date: Mon, 14 Oct 2024 15:51:51 +0800 Subject: [PATCH 292/541] add missing code of last commit (#9646) Signed-off-by: 21pages --- flutter/lib/web/bridge.dart | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/flutter/lib/web/bridge.dart b/flutter/lib/web/bridge.dart index 9238d054a6d5..d38f0f9cf393 100644 --- a/flutter/lib/web/bridge.dart +++ b/flutter/lib/web/bridge.dart @@ -1210,11 +1210,18 @@ class RustdeskImpl { Future mainLoadAb({dynamic hint}) { Completer completer = Completer(); + Future timeoutFuture = completer.future.timeout( + Duration(seconds: 2), + onTimeout: () { + completer.completeError(TimeoutException('Load ab timed out')); + return 'Timeout'; + }, + ); js.context["onLoadAbFinished"] = (String s) { completer.complete(s); }; js.context.callMethod('setByName', ['load_ab']); - return completer.future; + return timeoutFuture; } Future mainSaveGroup({required String json, dynamic hint}) { @@ -1228,11 +1235,18 @@ class RustdeskImpl { Future mainLoadGroup({dynamic hint}) { Completer completer = Completer(); + Future timeoutFuture = completer.future.timeout( + Duration(seconds: 2), + onTimeout: () { + completer.completeError(TimeoutException('Load group timed out')); + return 'Timeout'; + }, + ); js.context["onLoadGroupFinished"] = (String s) { completer.complete(s); }; js.context.callMethod('setByName', ['load_group']); - return completer.future; + return timeoutFuture; } Future sessionSendPointer( From 55187e92438af316732818998ff97e5aaddd72f0 Mon Sep 17 00:00:00 2001 From: 21pages Date: Mon, 14 Oct 2024 17:46:38 +0800 Subject: [PATCH 293/541] fix theme radio update (#9647) Signed-off-by: 21pages --- flutter/lib/common.dart | 2 +- flutter/lib/desktop/pages/desktop_setting_page.dart | 4 ++-- flutter/lib/main.dart | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 2bbb224cbb10..86f532d2abee 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -555,7 +555,7 @@ class MyTheme { return themeModeFromString(bind.mainGetLocalOption(key: kCommConfKeyTheme)); } - static void changeDarkMode(ThemeMode mode) async { + static Future changeDarkMode(ThemeMode mode) async { Get.changeThemeMode(mode); if (desktopType == DesktopType.main || isAndroid || isIOS || isWeb) { if (mode == ThemeMode.system) { diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index ebe55ca191b5..ada38ecf025e 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -369,8 +369,8 @@ class _GeneralState extends State<_General> { Widget theme() { final current = MyTheme.getThemeModePreference().toShortString(); - onChanged(String value) { - MyTheme.changeDarkMode(MyTheme.themeModeFromString(value)); + onChanged(String value) async { + await MyTheme.changeDarkMode(MyTheme.themeModeFromString(value)); setState(() {}); } diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index 18578f81b56f..16324c0e5c5c 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -507,7 +507,7 @@ _registerEventHandler() { platformFFI.registerEventHandler('theme', 'theme', (evt) async { String? dark = evt['dark']; if (dark != null) { - MyTheme.changeDarkMode(MyTheme.themeModeFromString(dark)); + await MyTheme.changeDarkMode(MyTheme.themeModeFromString(dark)); } }); platformFFI.registerEventHandler('language', 'language', (_) async { From 36e11c61a99d11642de90d437ffaa1f744626ad5 Mon Sep 17 00:00:00 2001 From: bovirus <1262554+bovirus@users.noreply.github.com> Date: Mon, 14 Oct 2024 15:26:55 +0200 Subject: [PATCH 294/541] Update Italian language (#9650) --- src/lang/it.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lang/it.rs b/src/lang/it.rs index 3aa0966d48ae..b791276b9406 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -648,8 +648,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Authentication Required", "Richiesta autenticazione"), ("Authenticate", "Autentica"), ("web_id_input_tip", "È possibile inserire un ID nello stesso server, nel client web non è supportato l'accesso con IP diretto.\nSe vuoi accedere ad un dispositivo in un altro server, aggiungi l'indirizzo del server (@?key=), ad esempio,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nSe vuoi accedere ad un dispositivo in un server pubblico, inserisci \"@public\", la chiave non è necessaria per il server pubblico."), - ("Download", ""), - ("Upload folder", ""), - ("Upload files", ""), + ("Download", "Download"), + ("Upload folder", "Cartella upload"), + ("Upload files", "File upload"), ].iter().cloned().collect(); } From 09083b3afaf2ebe57076bde0ec69c853a1b95013 Mon Sep 17 00:00:00 2001 From: Mr-Update <37781396+Mr-Update@users.noreply.github.com> Date: Wed, 16 Oct 2024 05:23:12 +0200 Subject: [PATCH 295/541] Update de.rs (#9668) --- src/lang/de.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lang/de.rs b/src/lang/de.rs index 11a0dbb3275e..f54cddac96d4 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -648,8 +648,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Authentication Required", "Authentifizierung erforderlich"), ("Authenticate", "Authentifizieren"), ("web_id_input_tip", "Sie können eine ID auf demselben Server eingeben, direkter IP-Zugriff wird im Web-Client nicht unterstützt.\nWenn Sie auf ein Gerät auf einem anderen Server zugreifen wollen, fügen Sie bitte die Serveradresse (@?key=) hinzu, zum Beispiel\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nWenn Sie auf ein Gerät auf einem öffentlichen Server zugreifen wollen, geben Sie bitte \"@public\" ein. Der Schlüssel wird für öffentliche Server nicht benötigt."), - ("Download", ""), - ("Upload folder", ""), - ("Upload files", ""), + ("Download", "Herunterladen"), + ("Upload folder", "Ordner hochladen"), + ("Upload files", "Dateien hochladen"), ].iter().cloned().collect(); } From ace98d98ad4089deda11135e0ce3001178cd6385 Mon Sep 17 00:00:00 2001 From: shleyZ Date: Wed, 16 Oct 2024 19:25:27 +0800 Subject: [PATCH 296/541] fix: TextFormField onChanged event triggered multiple times when Korean input (#9644) * fix: TextFormField onChanged event triggered multiple times when Korean input * logic fix for iOS * add comments * move 'onChanged' logic to handleSoftKeyboardInput --- flutter/lib/mobile/pages/remote_page.dart | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index 19f7cc57530d..e8a888696018 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -242,12 +242,16 @@ class _RemotePageState extends State { } } - // handle mobile virtual keyboard - void handleSoftKeyboardInput(String newValue) { + async void handleSoftKeyboardInput(String newValue) { if (isIOS) { - _handleIOSSoftKeyboardInput(newValue); + // fix: TextFormField onChanged event triggered multiple times when Korean input + // https://github.com/rustdesk/rustdesk/pull/9644 + await Future.delayed(const Duration(milliseconds: 10)); + + if (newValue != _textController.text) return; + return _handleIOSSoftKeyboardInput(_textController.text); } else { - _handleNonIOSSoftKeyboardInput(newValue); + return _handleNonIOSSoftKeyboardInput(newValue); } } From 1a0814b201485b23402c3687c0ead6e4f312f838 Mon Sep 17 00:00:00 2001 From: XLion Date: Wed, 16 Oct 2024 19:33:27 +0800 Subject: [PATCH 297/541] Update tw.rs (#9672) --- src/lang/tw.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 776151ae7c46..1f96700b03da 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -563,7 +563,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Plug out all", "拔出所有"), ("True color (4:4:4)", "全彩模式(4:4:4)"), ("Enable blocking user input", "允許封鎖使用者輸入"), - ("id_input_tip", "您可以輸入 ID、IP、或網域名稱+通訊埠號(<網域名稱>:<通訊埠號>)。\n如果您要存取位於其他伺服器上的設備,請在ID之後添加伺服器地址(@<伺服器地址>?key=<金鑰>)\n例如:9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=\n要存取公共伺服器上的設備,請輸入\"@public\",不需輸入金鑰。\n\n如果您想要在第一次連線時,強制使用中繼連接,請在 ID 的末尾添加 \"/r\",例如,\"9123456234/r\"。"), + ("id_input_tip", "您可以輸入 ID、IP、或網域名稱+通訊埠號(<網域名稱>:<通訊埠號>)。\n如果您要存取位於其他伺服器上的設備,請在 ID 之後添加伺服器地址(@<伺服器地址>?key=<金鑰>)\n例如:9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=\n要存取公共伺服器上的設備,請輸入\"@public\",不需輸入金鑰。\n\n如果您想要在第一次連線時,強制使用中繼連接,請在 ID 的末尾添加 \"/r\",例如,\"9123456234/r\"。"), ("privacy_mode_impl_mag_tip", "模式 1"), ("privacy_mode_impl_virtual_display_tip", "模式 2"), ("Enter privacy mode", "進入隱私模式"), @@ -647,9 +647,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("one-way-file-transfer-tip", "被控端啟用了單向文件傳輸"), ("Authentication Required", "需要身分驗證"), ("Authenticate", "認證"), - ("web_id_input_tip", ""), - ("Download", ""), - ("Upload folder", ""), - ("Upload files", ""), + ("web_id_input_tip", "您可以輸入同一個伺服器內的 ID,Web 客戶端不支援直接 IP 存取。\n如果您要存取位於其他伺服器上的設備,請在 ID 之後添加伺服器地址(@<伺服器地址>?key=<金鑰>)\n例如:9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=\n要存取公共伺服器上的設備,請輸入\"@public\",不需輸入金鑰。"), + ("Download", "下載"), + ("Upload folder", "上傳資料夾"), + ("Upload files", "上傳檔案"), ].iter().cloned().collect(); } From 5e920f0fd048e7e49046a17f833657a26d4c001a Mon Sep 17 00:00:00 2001 From: rustdesk Date: Wed, 16 Oct 2024 22:37:03 +0800 Subject: [PATCH 298/541] fix ci --- flutter/lib/mobile/pages/remote_page.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index e8a888696018..0072a2608db9 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -242,16 +242,16 @@ class _RemotePageState extends State { } } - async void handleSoftKeyboardInput(String newValue) { + Future handleSoftKeyboardInput(String newValue) async { if (isIOS) { // fix: TextFormField onChanged event triggered multiple times when Korean input // https://github.com/rustdesk/rustdesk/pull/9644 await Future.delayed(const Duration(milliseconds: 10)); if (newValue != _textController.text) return; - return _handleIOSSoftKeyboardInput(_textController.text); + _handleIOSSoftKeyboardInput(_textController.text); } else { - return _handleNonIOSSoftKeyboardInput(newValue); + _handleNonIOSSoftKeyboardInput(newValue); } } From ae8dfe84a078ba043386dbb06ccf6f2b6c9c65f3 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Thu, 17 Oct 2024 17:23:09 +0800 Subject: [PATCH 299/541] feat, web toast (#9686) Signed-off-by: fufesou --- flutter/lib/models/model.dart | 34 ++++++++++++++++++++++++++++++++++ src/lang/ar.rs | 1 + src/lang/be.rs | 1 + src/lang/bg.rs | 1 + src/lang/ca.rs | 1 + src/lang/cn.rs | 1 + src/lang/cs.rs | 1 + src/lang/da.rs | 1 + src/lang/de.rs | 1 + src/lang/el.rs | 1 + src/lang/eo.rs | 1 + src/lang/es.rs | 1 + src/lang/et.rs | 1 + src/lang/eu.rs | 1 + src/lang/fa.rs | 1 + src/lang/fr.rs | 1 + src/lang/he.rs | 1 + src/lang/hr.rs | 1 + src/lang/hu.rs | 1 + src/lang/id.rs | 1 + src/lang/it.rs | 1 + src/lang/ja.rs | 1 + src/lang/ko.rs | 1 + src/lang/kz.rs | 1 + src/lang/lt.rs | 1 + src/lang/lv.rs | 1 + src/lang/nb.rs | 1 + src/lang/nl.rs | 1 + src/lang/pl.rs | 1 + src/lang/pt_PT.rs | 1 + src/lang/ptbr.rs | 1 + src/lang/ro.rs | 1 + src/lang/ru.rs | 1 + src/lang/sk.rs | 1 + src/lang/sl.rs | 1 + src/lang/sq.rs | 1 + src/lang/sr.rs | 1 + src/lang/sv.rs | 1 + src/lang/template.rs | 1 + src/lang/th.rs | 1 + src/lang/tr.rs | 1 + src/lang/tw.rs | 1 + src/lang/uk.rs | 1 + src/lang/vn.rs | 1 + 44 files changed, 77 insertions(+) diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 2833f38349e4..71effd797e2d 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -4,6 +4,7 @@ import 'dart:math'; import 'dart:typed_data'; import 'dart:ui' as ui; +import 'package:bot_toast/bot_toast.dart'; import 'package:desktop_multi_window/desktop_multi_window.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -269,6 +270,8 @@ class FfiModel with ChangeNotifier { var name = evt['name']; if (name == 'msgbox') { handleMsgBox(evt, sessionId, peerId); + } else if (name == 'toast') { + handleToast(evt, sessionId, peerId); } else if (name == 'set_multiple_windows_session') { handleMultipleWindowsSession(evt, sessionId, peerId); } else if (name == 'peer_info') { @@ -595,6 +598,37 @@ class FfiModel with ChangeNotifier { } } + handleToast(Map evt, SessionID sessionId, String peerId) { + final type = evt['type'] ?? 'info'; + final text = evt['text'] ?? ''; + final durMsc = evt['dur_msec'] ?? 2000; + final duration = Duration(milliseconds: durMsc); + if ((text).isEmpty) { + BotToast.showLoading( + duration: duration, + clickClose: true, + allowClick: true, + ); + } else { + if (type.contains('error')) { + BotToast.showText( + contentColor: Colors.red, + text: translate(text), + duration: duration, + clickClose: true, + onlyOne: true, + ); + } else { + BotToast.showText( + text: translate(text), + duration: duration, + clickClose: true, + onlyOne: true, + ); + } + } + } + /// Show a message box with [type], [title] and [text]. showMsgBox(SessionID sessionId, String type, String title, String text, String link, bool hasRetry, OverlayDialogManager dialogManager, diff --git a/src/lang/ar.rs b/src/lang/ar.rs index 879414cd613f..6cae020b6e45 100644 --- a/src/lang/ar.rs +++ b/src/lang/ar.rs @@ -651,5 +651,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Download", ""), ("Upload folder", ""), ("Upload files", ""), + ("Clipboard is synchronized", ""), ].iter().cloned().collect(); } diff --git a/src/lang/be.rs b/src/lang/be.rs index 501438fd0e4f..7e9f928ae573 100644 --- a/src/lang/be.rs +++ b/src/lang/be.rs @@ -651,5 +651,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Download", ""), ("Upload folder", ""), ("Upload files", ""), + ("Clipboard is synchronized", ""), ].iter().cloned().collect(); } diff --git a/src/lang/bg.rs b/src/lang/bg.rs index cf972ee766ca..9f6282e75234 100644 --- a/src/lang/bg.rs +++ b/src/lang/bg.rs @@ -651,5 +651,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Download", ""), ("Upload folder", ""), ("Upload files", ""), + ("Clipboard is synchronized", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ca.rs b/src/lang/ca.rs index 4001c1e6e69b..64748c9ac093 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -651,5 +651,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Download", ""), ("Upload folder", ""), ("Upload files", ""), + ("Clipboard is synchronized", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 17b3ba159af5..5ad91ada84f7 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -651,5 +651,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Download", "下载"), ("Upload folder", "上传文件夹"), ("Upload files", "上传文件"), + ("Clipboard is synchronized", "剪贴板已同步"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index 8404ee3e41da..8db1470b98a5 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -651,5 +651,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Download", ""), ("Upload folder", ""), ("Upload files", ""), + ("Clipboard is synchronized", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index c59f3efc9c7a..f867cac467a8 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -651,5 +651,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Download", ""), ("Upload folder", ""), ("Upload files", ""), + ("Clipboard is synchronized", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index f54cddac96d4..73e57183d0da 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -651,5 +651,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Download", "Herunterladen"), ("Upload folder", "Ordner hochladen"), ("Upload files", "Dateien hochladen"), + ("Clipboard is synchronized", ""), ].iter().cloned().collect(); } diff --git a/src/lang/el.rs b/src/lang/el.rs index eaa31c55fadb..625867c343e9 100644 --- a/src/lang/el.rs +++ b/src/lang/el.rs @@ -651,5 +651,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Download", ""), ("Upload folder", ""), ("Upload files", ""), + ("Clipboard is synchronized", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index e7daf0ff7957..217b37a6a5e6 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -651,5 +651,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Download", ""), ("Upload folder", ""), ("Upload files", ""), + ("Clipboard is synchronized", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index bd7e4df153d5..70bd9d9e52fe 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -651,5 +651,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Download", ""), ("Upload folder", ""), ("Upload files", ""), + ("Clipboard is synchronized", ""), ].iter().cloned().collect(); } diff --git a/src/lang/et.rs b/src/lang/et.rs index 131e3b2156b4..8187759d5755 100644 --- a/src/lang/et.rs +++ b/src/lang/et.rs @@ -651,5 +651,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Download", ""), ("Upload folder", ""), ("Upload files", ""), + ("Clipboard is synchronized", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eu.rs b/src/lang/eu.rs index 267431efd1d2..f8ba679a756d 100644 --- a/src/lang/eu.rs +++ b/src/lang/eu.rs @@ -651,5 +651,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Download", ""), ("Upload folder", ""), ("Upload files", ""), + ("Clipboard is synchronized", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index c070f18e20dd..cf5ce0f0f70e 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -651,5 +651,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Download", ""), ("Upload folder", ""), ("Upload files", ""), + ("Clipboard is synchronized", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 2fa41f3fe6e6..0fd093688b1f 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -651,5 +651,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Download", ""), ("Upload folder", ""), ("Upload files", ""), + ("Clipboard is synchronized", ""), ].iter().cloned().collect(); } diff --git a/src/lang/he.rs b/src/lang/he.rs index 5d5e4637e810..e36252afe903 100644 --- a/src/lang/he.rs +++ b/src/lang/he.rs @@ -651,5 +651,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Download", ""), ("Upload folder", ""), ("Upload files", ""), + ("Clipboard is synchronized", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hr.rs b/src/lang/hr.rs index 378f0e90977c..e2480eb63502 100644 --- a/src/lang/hr.rs +++ b/src/lang/hr.rs @@ -651,5 +651,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Download", ""), ("Upload folder", ""), ("Upload files", ""), + ("Clipboard is synchronized", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 8aaaa3a13a7d..00c56edfbcad 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -651,5 +651,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Download", ""), ("Upload folder", ""), ("Upload files", ""), + ("Clipboard is synchronized", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index 44703afc6669..2f95411f0823 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -651,5 +651,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Download", ""), ("Upload folder", ""), ("Upload files", ""), + ("Clipboard is synchronized", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index b791276b9406..8fff93a6edf0 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -651,5 +651,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Download", "Download"), ("Upload folder", "Cartella upload"), ("Upload files", "File upload"), + ("Clipboard is synchronized", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 41fcca10ce24..dbc40b789796 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -651,5 +651,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Download", ""), ("Upload folder", ""), ("Upload files", ""), + ("Clipboard is synchronized", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 5c9660560d0d..966ac90097e8 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -651,5 +651,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Download", ""), ("Upload folder", ""), ("Upload files", ""), + ("Clipboard is synchronized", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index 3b5aba5e4cd0..d62bbc393bf8 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -651,5 +651,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Download", ""), ("Upload folder", ""), ("Upload files", ""), + ("Clipboard is synchronized", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lt.rs b/src/lang/lt.rs index c6915aab3171..bc7d856b1e8e 100644 --- a/src/lang/lt.rs +++ b/src/lang/lt.rs @@ -651,5 +651,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Download", ""), ("Upload folder", ""), ("Upload files", ""), + ("Clipboard is synchronized", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lv.rs b/src/lang/lv.rs index 4a24a38a0cb1..fa266ac2e314 100644 --- a/src/lang/lv.rs +++ b/src/lang/lv.rs @@ -651,5 +651,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Download", ""), ("Upload folder", ""), ("Upload files", ""), + ("Clipboard is synchronized", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nb.rs b/src/lang/nb.rs index a5b501aa46cb..d91eb93258d5 100644 --- a/src/lang/nb.rs +++ b/src/lang/nb.rs @@ -651,5 +651,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Download", ""), ("Upload folder", ""), ("Upload files", ""), + ("Clipboard is synchronized", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nl.rs b/src/lang/nl.rs index 6263cccb8e35..b780ce7daf72 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -651,5 +651,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Download", ""), ("Upload folder", ""), ("Upload files", ""), + ("Clipboard is synchronized", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 8479949b3176..8e9989084491 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -651,5 +651,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Download", ""), ("Upload folder", ""), ("Upload files", ""), + ("Clipboard is synchronized", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index a30692c42e1c..4c01d0b628a2 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -651,5 +651,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Download", ""), ("Upload folder", ""), ("Upload files", ""), + ("Clipboard is synchronized", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index 45dc2259ba84..240fae99ae42 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -651,5 +651,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Download", ""), ("Upload folder", ""), ("Upload files", ""), + ("Clipboard is synchronized", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ro.rs b/src/lang/ro.rs index 50687686544d..26858b134363 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -651,5 +651,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Download", ""), ("Upload folder", ""), ("Upload files", ""), + ("Clipboard is synchronized", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 4bc1516d1159..d011eae60068 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -651,5 +651,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Download", ""), ("Upload folder", ""), ("Upload files", ""), + ("Clipboard is synchronized", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index dabec3ed3dec..28e322460df7 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -651,5 +651,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Download", ""), ("Upload folder", ""), ("Upload files", ""), + ("Clipboard is synchronized", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sl.rs b/src/lang/sl.rs index f834687c78fb..e7f6248160be 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -651,5 +651,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Download", ""), ("Upload folder", ""), ("Upload files", ""), + ("Clipboard is synchronized", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sq.rs b/src/lang/sq.rs index acc5a627f899..58dc1ed5502d 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -651,5 +651,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Download", ""), ("Upload folder", ""), ("Upload files", ""), + ("Clipboard is synchronized", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sr.rs b/src/lang/sr.rs index 0ade32f819db..d38d20e9ef34 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -651,5 +651,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Download", ""), ("Upload folder", ""), ("Upload files", ""), + ("Clipboard is synchronized", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sv.rs b/src/lang/sv.rs index 5dd93ea4ea2c..cfbac29b9034 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -651,5 +651,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Download", ""), ("Upload folder", ""), ("Upload files", ""), + ("Clipboard is synchronized", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index eb62d5621cf9..1c4606e94797 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -651,5 +651,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Download", ""), ("Upload folder", ""), ("Upload files", ""), + ("Clipboard is synchronized", ""), ].iter().cloned().collect(); } diff --git a/src/lang/th.rs b/src/lang/th.rs index deb7f974191f..2dbb564e870e 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -651,5 +651,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Download", ""), ("Upload folder", ""), ("Upload files", ""), + ("Clipboard is synchronized", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index 656feb0ef079..9bdbec110c60 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -651,5 +651,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Download", ""), ("Upload folder", ""), ("Upload files", ""), + ("Clipboard is synchronized", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 1f96700b03da..b37949206ace 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -651,5 +651,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Download", "下載"), ("Upload folder", "上傳資料夾"), ("Upload files", "上傳檔案"), + ("Clipboard is synchronized", ""), ].iter().cloned().collect(); } diff --git a/src/lang/uk.rs b/src/lang/uk.rs index 35ffa1c290da..2bce8cc81735 100644 --- a/src/lang/uk.rs +++ b/src/lang/uk.rs @@ -651,5 +651,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Download", ""), ("Upload folder", ""), ("Upload files", ""), + ("Clipboard is synchronized", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index d07225ba0a4d..9693d35c3c8e 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -651,5 +651,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Download", ""), ("Upload folder", ""), ("Upload files", ""), + ("Clipboard is synchronized", ""), ].iter().cloned().collect(); } From defb3e6c73c4fc74bf315cc238bcb5339d24e887 Mon Sep 17 00:00:00 2001 From: 21pages Date: Thu, 17 Oct 2024 20:05:13 +0800 Subject: [PATCH 300/541] fix gtk-sudo in non-English linux (#9680) change LC_ALL from C.UTF-8 to C Signed-off-by: 21pages --- src/platform/gtk_sudo.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/platform/gtk_sudo.rs b/src/platform/gtk_sudo.rs index 0105b335c311..9aeea1e2b01c 100644 --- a/src/platform/gtk_sudo.rs +++ b/src/platform/gtk_sudo.rs @@ -505,7 +505,7 @@ fn child(su_user: Option, args: Vec) -> ResultType<()> { command = format!("'{}'", quote_shell_arg(&command, false)); } params.push(command); - std::env::set_var("LC_ALL", "C.UTF-8"); + std::env::set_var("LC_ALL", "C"); if let Some(user) = &su_user { let su_subcommand = params From 53d11e99d73eaf48fa8bf1a2c47796485c57c532 Mon Sep 17 00:00:00 2001 From: 21pages Date: Fri, 18 Oct 2024 08:45:16 +0800 Subject: [PATCH 301/541] web only decode the latest image (#9689) 1. web only decode the latest image 2. web/ios remove relay server config when import Signed-off-by: 21pages --- flutter/lib/common.dart | 3 +++ flutter/lib/models/model.dart | 21 +++++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 86f532d2abee..8bf8cb056714 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -3177,6 +3177,9 @@ importConfig(List? controllers, List? errMsgs, if (text != null && text.isNotEmpty) { try { final sc = ServerConfig.decode(text); + if (isWeb || isIOS) { + sc.relayServer = ''; + } if (sc.idServer.isNotEmpty) { Future success = setServerConfig(controllers, errMsgs, sc); success.then((value) { diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 71effd797e2d..d836f62a6e8a 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -1225,6 +1225,27 @@ class ImageModel with ChangeNotifier { clearImage() => _image = null; + bool _webDecodingRgba = false; + final List _webRgbaList = List.empty(growable: true); + webOnRgba(int display, Uint8List rgba) async { + // deep copy needed, otherwise "instantiateCodec failed: TypeError: Cannot perform Construct on a detached ArrayBuffer" + _webRgbaList.add(Uint8List.fromList(rgba)); + if (_webDecodingRgba) { + return; + } + _webDecodingRgba = true; + try { + while (_webRgbaList.isNotEmpty) { + final rgba2 = _webRgbaList.last; + _webRgbaList.clear(); + await decodeAndUpdate(display, rgba2); + } + } catch (e) { + debugPrint('onRgba error: $e'); + } + _webDecodingRgba = false; + } + onRgba(int display, Uint8List rgba) async { try { await decodeAndUpdate(display, rgba); From 0d3243e6dd07aca3e770b78cb1cbc90ae6eda3ab Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Fri, 18 Oct 2024 08:55:18 +0800 Subject: [PATCH 302/541] fix: android, Korean input (#9667) Signed-off-by: fufesou --- flutter/lib/mobile/pages/remote_page.dart | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index 0072a2608db9..395ce333365b 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -57,6 +57,9 @@ class _RemotePageState extends State { final TextEditingController _textController = TextEditingController(text: initText); + // This timer is used to check the composing status of the soft keyboard. + // It is used for Android, Korean(and other similar) input method. + Timer? _composingTimer; _RemotePageState(String id) { initSharedStates(id); @@ -104,6 +107,7 @@ class _RemotePageState extends State { _physicalFocusNode.dispose(); await gFFI.close(); _timer?.cancel(); + _composingTimer?.cancel(); gFFI.dialogManager.dismissAll(); await SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: SystemUiOverlay.values); @@ -139,6 +143,7 @@ class _RemotePageState extends State { gFFI.ffiModel.pi.version.isNotEmpty) { gFFI.invokeMethod("enable_soft_keyboard", false); } + _composingTimer?.cancel(); } else { _timer?.cancel(); _timer = Timer(kMobileDelaySoftKeyboardFocus, () { @@ -202,6 +207,13 @@ class _RemotePageState extends State { } void _handleNonIOSSoftKeyboardInput(String newValue) { + _composingTimer?.cancel(); + if (_textController.value.isComposingRangeValid) { + _composingTimer = Timer(Duration(milliseconds: 25), () { + _handleNonIOSSoftKeyboardInput(_textController.value.text); + }); + return; + } var oldValue = _value; _value = newValue; if (oldValue.isNotEmpty && From 0f6d28def724f0392974f5b3bba2f9d4735692c3 Mon Sep 17 00:00:00 2001 From: flusheDData <116861809+flusheDData@users.noreply.github.com> Date: Fri, 18 Oct 2024 02:55:39 +0200 Subject: [PATCH 303/541] Update es.rs (#9687) New terms added --- src/lang/es.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/lang/es.rs b/src/lang/es.rs index 70bd9d9e52fe..25cd7bf283c2 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -647,10 +647,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("one-way-file-transfer-tip", "La transferencia en un sentido está habilitada en el lado controlado."), ("Authentication Required", "Se requiere autenticación"), ("Authenticate", "Autenticar"), - ("web_id_input_tip", ""), - ("Download", ""), - ("Upload folder", ""), - ("Upload files", ""), - ("Clipboard is synchronized", ""), + ("web_id_input_tip", "Puedes introducir una ID en el mismo servidor, el cliente web no soporta acceso vía IP.\nSi quieres acceder a un dispositivo en otro servidor, por favor, agrega la dirección del servidor (@?clave=), por ejemplo,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nSi quieres accedder a un dispositivo en un servidor público, por favor, introduce \"@public\", la clave no es necesaria para el servidor público."), + ("Download", "Descarga"), + ("Upload folder", "Subir carpeta"), + ("Upload files", "Subir archivos"), + ("Clipboard is synchronized", "Portapapeles sincronizado"), ].iter().cloned().collect(); } From 844caf8c15c3fcf5de4ab6cb38653580a12c0244 Mon Sep 17 00:00:00 2001 From: bovirus <1262554+bovirus@users.noreply.github.com> Date: Fri, 18 Oct 2024 10:07:49 +0200 Subject: [PATCH 304/541] Update Italian language (#9692) --- src/lang/it.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lang/it.rs b/src/lang/it.rs index 8fff93a6edf0..baaa1d10d31e 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -651,6 +651,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Download", "Download"), ("Upload folder", "Cartella upload"), ("Upload files", "File upload"), - ("Clipboard is synchronized", ""), + ("Clipboard is synchronized", "Gli appunti sono sincronizzati"), ].iter().cloned().collect(); } From 675ffe038164c802233428736af013a1e0e1e844 Mon Sep 17 00:00:00 2001 From: Kleofass <4000163+Kleofass@users.noreply.github.com> Date: Fri, 18 Oct 2024 17:51:17 +0300 Subject: [PATCH 305/541] Update lv.rs (#9694) --- src/lang/lv.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/lang/lv.rs b/src/lang/lv.rs index fa266ac2e314..bf67345c3a41 100644 --- a/src/lang/lv.rs +++ b/src/lang/lv.rs @@ -647,10 +647,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("one-way-file-transfer-tip", "Kontrolējamajā pusē ir iespējota vienvirziena failu pārsūtīšana."), ("Authentication Required", "Nepieciešama autentifikācija"), ("Authenticate", "Autentificēt"), - ("web_id_input_tip", ""), - ("Download", ""), - ("Upload folder", ""), - ("Upload files", ""), - ("Clipboard is synchronized", ""), + ("web_id_input_tip", "Varat ievadīt ID tajā pašā serverī, tīmekļa klientā tiešā IP piekļuve netiek atbalstīta.\nJa vēlaties piekļūt ierīcei citā serverī, lūdzu, pievienojiet servera adresi (@?key=), piemēram,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nJa vēlaties piekļūt ierīcei publiskajā serverī, lūdzu, ievadiet \"@public\", publiskajam serverim atslēga nav nepieciešama."), + ("Download", "Lejupielādēt"), + ("Upload folder", "Augšupielādēt mapi"), + ("Upload files", "Augšupielādēt failus"), + ("Clipboard is synchronized", "Starpliktuve ir sinhronizēta"), ].iter().cloned().collect(); } From 8c8a643ccec7c37cce42f2daae7e63caa444bf85 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Sat, 19 Oct 2024 10:57:42 +0800 Subject: [PATCH 306/541] fix: workaround physical display rotation (#9696) Signed-off-by: fufesou --- libs/scrap/src/common/mod.rs | 8 ++++---- libs/scrap/src/common/vram.rs | 7 ++++++- libs/scrap/src/dxgi/mod.rs | 12 +++++++++++- 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/libs/scrap/src/common/mod.rs b/libs/scrap/src/common/mod.rs index d6060e1315f8..635f0ec26d45 100644 --- a/libs/scrap/src/common/mod.rs +++ b/libs/scrap/src/common/mod.rs @@ -156,7 +156,7 @@ pub trait TraitPixelBuffer { #[cfg(not(any(target_os = "ios")))] pub enum Frame<'a> { PixelBuffer(PixelBuffer<'a>), - Texture(*mut c_void), + Texture((*mut c_void, usize)), } #[cfg(not(any(target_os = "ios")))] @@ -164,7 +164,7 @@ impl Frame<'_> { pub fn valid<'a>(&'a self) -> bool { match self { Frame::PixelBuffer(pixelbuffer) => !pixelbuffer.data().is_empty(), - Frame::Texture(texture) => !texture.is_null(), + Frame::Texture((texture, _)) => !texture.is_null(), } } @@ -186,7 +186,7 @@ impl Frame<'_> { pub enum EncodeInput<'a> { YUV(&'a [u8]), - Texture(*mut c_void), + Texture((*mut c_void, usize)), } impl<'a> EncodeInput<'a> { @@ -197,7 +197,7 @@ impl<'a> EncodeInput<'a> { } } - pub fn texture(&self) -> ResultType<*mut c_void> { + pub fn texture(&self) -> ResultType<(*mut c_void, usize)> { match self { Self::Texture(f) => Ok(*f), _ => bail!("not texture frame"), diff --git a/libs/scrap/src/common/vram.rs b/libs/scrap/src/common/vram.rs index a2b4d348c46f..aae961df6143 100644 --- a/libs/scrap/src/common/vram.rs +++ b/libs/scrap/src/common/vram.rs @@ -101,7 +101,12 @@ impl EncoderApi for VRamEncoder { frame: EncodeInput, ms: i64, ) -> ResultType { - let texture = frame.texture()?; + let (texture, rotation) = frame.texture()?; + if rotation != 0 { + // to-do: support rotation + // Both the encoder and display(w,h) information need to be changed. + bail!("rotation not supported"); + } let mut vf = VideoFrame::new(); let mut frames = Vec::new(); for frame in self diff --git a/libs/scrap/src/dxgi/mod.rs b/libs/scrap/src/dxgi/mod.rs index abd1f5026999..33a60e7d9919 100644 --- a/libs/scrap/src/dxgi/mod.rs +++ b/libs/scrap/src/dxgi/mod.rs @@ -253,7 +253,17 @@ impl Capturer { pub fn frame<'a>(&'a mut self, timeout: UINT) -> io::Result> { if self.output_texture { - Ok(Frame::Texture(self.get_texture(timeout)?)) + let rotation = match self.display.rotation() { + DXGI_MODE_ROTATION_IDENTITY | DXGI_MODE_ROTATION_UNSPECIFIED => 0, + DXGI_MODE_ROTATION_ROTATE90 => 90, + DXGI_MODE_ROTATION_ROTATE180 => 180, + DXGI_MODE_ROTATION_ROTATE270 => 270, + _ => { + // Unsupported rotation, try anyway + 0 + } + }; + Ok(Frame::Texture((self.get_texture(timeout)?, rotation))) } else { let width = self.width; let height = self.height; From 1212d9fa2df6b0f5d2e1970e1a0e4743dc95b4a7 Mon Sep 17 00:00:00 2001 From: 21pages Date: Sat, 19 Oct 2024 15:32:17 +0800 Subject: [PATCH 307/541] web uni link (#9697) Signed-off-by: 21pages --- flutter/lib/common.dart | 11 ++++- flutter/lib/main.dart | 2 +- flutter/lib/mobile/pages/home_page.dart | 60 +++++++++++++++++++++++++ 3 files changed, 70 insertions(+), 3 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 8bf8cb056714..1d59d0202d81 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -2038,6 +2038,8 @@ Future restoreWindowPosition(WindowType type, return false; } +var webInitialLink = ""; + /// Initialize uni links for macos/windows /// /// [Availability] @@ -2054,7 +2056,12 @@ Future initUniLinks() async { if (initialLink == null || initialLink.isEmpty) { return false; } - return handleUriLink(uriString: initialLink); + if (isWeb) { + webInitialLink = initialLink; + return false; + } else { + return handleUriLink(uriString: initialLink); + } } catch (err) { debugPrintStack(label: "$err"); return false; @@ -2067,7 +2074,7 @@ Future initUniLinks() async { /// /// Returns a [StreamSubscription] which can listen the uni links. StreamSubscription? listenUniLinks({handleByFlutter = true}) { - if (isLinux) { + if (isLinux || isWeb) { return null; } diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index 16324c0e5c5c..00afbb001e76 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -162,7 +162,7 @@ void runMobileApp() async { await Future.wait([gFFI.abModel.loadCache(), gFFI.groupModel.loadCache()]); gFFI.userModel.refreshCurrentUser(); runApp(App()); - if (!isWeb) await initUniLinks(); + await initUniLinks(); } void runMultiWindow( diff --git a/flutter/lib/mobile/pages/home_page.dart b/flutter/lib/mobile/pages/home_page.dart index bad569afeabe..efccc5de65ef 100644 --- a/flutter/lib/mobile/pages/home_page.dart +++ b/flutter/lib/mobile/pages/home_page.dart @@ -161,6 +161,7 @@ class WebHomePage extends StatelessWidget { @override Widget build(BuildContext context) { stateGlobal.isInMainPage = true; + handleUnilink(context); return Scaffold( // backgroundColor: MyTheme.grayBg, appBar: AppBar( @@ -171,4 +172,63 @@ class WebHomePage extends StatelessWidget { body: connectionPage, ); } + + handleUnilink(BuildContext context) { + if (webInitialLink.isEmpty) { + return; + } + final link = webInitialLink; + webInitialLink = ''; + final splitter = ["/#/", "/#", "#/", "#"]; + var fakelink = ''; + for (var s in splitter) { + if (link.contains(s)) { + var list = link.split(s); + if (list.length < 2 || list[1].isEmpty) { + return; + } + list.removeAt(0); + fakelink = "rustdesk://${list.join(s)}"; + break; + } + } + if (fakelink.isEmpty) { + return; + } + final uri = Uri.tryParse(fakelink); + if (uri == null) { + return; + } + final args = urlLinkToCmdArgs(uri); + if (args == null || args.isEmpty) { + return; + } + bool isFileTransfer = false; + String? id; + String? password; + for (int i = 0; i < args.length; i++) { + switch (args[i]) { + case '--connect': + case '--play': + isFileTransfer = false; + id = args[i + 1]; + i++; + break; + case '--file-transfer': + isFileTransfer = true; + id = args[i + 1]; + i++; + break; + case '--password': + password = args[i + 1]; + i++; + break; + default: + break; + } + } + if (id != null) { + connect(context, id, isFileTransfer: isFileTransfer, password: password); + } + } } From 1bf4ef1f46ea49b6941d3334cc5fe0a2a6ed1552 Mon Sep 17 00:00:00 2001 From: Mr-Update <37781396+Mr-Update@users.noreply.github.com> Date: Sat, 19 Oct 2024 13:41:14 +0200 Subject: [PATCH 308/541] Update de.rs (#9699) --- src/lang/de.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lang/de.rs b/src/lang/de.rs index 73e57183d0da..4213c86de740 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -651,6 +651,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Download", "Herunterladen"), ("Upload folder", "Ordner hochladen"), ("Upload files", "Dateien hochladen"), - ("Clipboard is synchronized", ""), + ("Clipboard is synchronized", "Zwischenablage ist synchronisiert"), ].iter().cloned().collect(); } From 547da310957f597ae6d0608cc5f329b9f9ec6a77 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Sat, 19 Oct 2024 19:41:55 +0800 Subject: [PATCH 309/541] fix: windows, exit fullscreen, remove redraw (#9700) Signed-off-by: fufesou --- flutter/lib/models/state_model.dart | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/flutter/lib/models/state_model.dart b/flutter/lib/models/state_model.dart index 3481aa2b3e1b..f8f06cc3fb8a 100644 --- a/flutter/lib/models/state_model.dart +++ b/flutter/lib/models/state_model.dart @@ -101,15 +101,8 @@ class StateGlobal { if (procWnd) { final wc = WindowController.fromWindowId(windowId); wc.setFullscreen(_fullscreen.isTrue).then((_) { - // https://github.com/leanflutter/window_manager/issues/131#issuecomment-1111587982 - if (isWindows && _fullscreen.isFalse) { - Future.delayed(Duration.zero, () async { - final frame = await wc.getFrame(); - final newRect = Rect.fromLTWH( - frame.left, frame.top, frame.width + 1, frame.height + 1); - await wc.setFrame(newRect); - }); - } + // We remove the redraw (width + 1, height + 1), because this issue cannot be reproduced. + // https://github.com/rustdesk/rustdesk/issues/9675 }); } } From 289076aa705870f3849dd5c4f571b2a7d14512bd Mon Sep 17 00:00:00 2001 From: solokot Date: Mon, 21 Oct 2024 09:12:58 +0300 Subject: [PATCH 310/541] Update ru.rs (#9712) --- src/lang/ru.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/lang/ru.rs b/src/lang/ru.rs index d011eae60068..00ae750f27db 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -147,7 +147,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("OS Password", "Пароль входа в ОС"), ("install_tip", "В некоторых случаях из-за UAC RustDesk может работать неправильно на удалённом узле. Чтобы избежать возможных проблем с UAC, нажмите кнопку ниже для установки RustDesk в системе."), ("Click to upgrade", "Нажмите, чтобы обновить"), - ("Click to download", "Нажмите, чтобы загрузить"), + ("Click to download", "Нажмите, чтобы скачать"), ("Click to update", "Нажмите, чтобы обновить"), ("Configure", "Настроить"), ("config_acc", "Чтобы удалённо управлять своим рабочим столом, вы должны предоставить RustDesk права \"доступа\""), @@ -648,9 +648,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Authentication Required", "Требуется аутентификация"), ("Authenticate", "Аутентификация"), ("web_id_input_tip", "Можно ввести ID на том же сервере, прямой доступ по IP в веб-клиенте не поддерживается.\nЕсли вы хотите получить доступ к устройству на другом сервере, добавьте адрес сервера (@<адрес_сервера>?key=<ключ>), например,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nЕсли вы хотите получить доступ к устройству на публичном сервере, введите \"@public\", для публичного сервера ключ не нужен."), - ("Download", ""), - ("Upload folder", ""), - ("Upload files", ""), - ("Clipboard is synchronized", ""), + ("Download", "Скачать"), + ("Upload folder", "Загрузить папку"), + ("Upload files", "Загрузить файлы"), + ("Clipboard is synchronized", "Буфер обмена синхронизирован"), ].iter().cloned().collect(); } From e8187588c14150d1b8102650ca33d2052a1c434b Mon Sep 17 00:00:00 2001 From: 21pages Date: Mon, 21 Oct 2024 14:34:06 +0800 Subject: [PATCH 311/541] auto record outgoing (#9711) * Add option auto record outgoing session * In the same connection, all displays and all windows share the same recording state. todo: Android check external storage permission Known issue: * Sciter old issue, stop the process directly without stop record, the record file can't play. Signed-off-by: 21pages --- Cargo.lock | 2 +- flutter/lib/consts.dart | 1 + .../desktop/pages/desktop_setting_page.dart | 93 +++---- flutter/lib/desktop/pages/remote_page.dart | 3 +- .../lib/desktop/widgets/remote_toolbar.dart | 3 +- flutter/lib/mobile/pages/remote_page.dart | 9 +- flutter/lib/mobile/pages/settings_page.dart | 73 ++++-- flutter/lib/models/model.dart | 73 +----- libs/hbb_common/src/config.rs | 6 + libs/scrap/Cargo.toml | 1 - libs/scrap/src/common/codec.rs | 12 +- libs/scrap/src/common/mod.rs | 29 +++ libs/scrap/src/common/record.rs | 234 ++++++++++-------- src/client.rs | 72 +++--- src/client/io_loop.rs | 8 +- src/flutter.rs | 7 +- src/flutter_ffi.rs | 16 +- src/lang/ar.rs | 1 + src/lang/be.rs | 1 + src/lang/bg.rs | 1 + src/lang/ca.rs | 1 + src/lang/cn.rs | 3 +- src/lang/cs.rs | 1 + src/lang/da.rs | 1 + src/lang/de.rs | 1 + src/lang/el.rs | 1 + src/lang/eo.rs | 1 + src/lang/es.rs | 1 + src/lang/et.rs | 1 + src/lang/eu.rs | 1 + src/lang/fa.rs | 1 + src/lang/fr.rs | 1 + src/lang/he.rs | 1 + src/lang/hr.rs | 1 + src/lang/hu.rs | 1 + src/lang/id.rs | 1 + src/lang/it.rs | 1 + src/lang/ja.rs | 1 + src/lang/ko.rs | 1 + src/lang/kz.rs | 1 + src/lang/lt.rs | 1 + src/lang/lv.rs | 1 + src/lang/nb.rs | 1 + src/lang/nl.rs | 1 + src/lang/pl.rs | 1 + src/lang/pt_PT.rs | 1 + src/lang/ptbr.rs | 1 + src/lang/ro.rs | 1 + src/lang/ru.rs | 1 + src/lang/sk.rs | 1 + src/lang/sl.rs | 1 + src/lang/sq.rs | 1 + src/lang/sr.rs | 1 + src/lang/sv.rs | 1 + src/lang/template.rs | 1 + src/lang/th.rs | 1 + src/lang/tr.rs | 1 + src/lang/tw.rs | 1 + src/lang/uk.rs | 1 + src/lang/vn.rs | 1 + src/server/video_service.rs | 24 +- src/ui/header.tis | 22 +- src/ui/index.tis | 3 + src/ui/remote.rs | 8 +- src/ui_session_interface.rs | 20 +- 65 files changed, 442 insertions(+), 322 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e1474f2f3a58..923325c4ea79 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3051,7 +3051,7 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hwcodec" version = "0.7.0" -source = "git+https://github.com/rustdesk-org/hwcodec#f74410edec91435252b8394c38f8eeca87ad2a26" +source = "git+https://github.com/rustdesk-org/hwcodec#8bbd05bb300ad07cc345356ad85570f9ea99fbfa" dependencies = [ "bindgen 0.59.2", "cc", diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index 1be9c7712c79..89306bb7ae89 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -89,6 +89,7 @@ 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 kOptionAllowAutoRecordOutgoing = "allow-auto-record-outgoing"; const String kOptionVideoSaveDirectory = "video-save-directory"; const String kOptionAccessMode = "access-mode"; const String kOptionEnableKeyboard = "enable-keyboard"; diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index ada38ecf025e..766c160e042c 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -575,12 +575,17 @@ class _GeneralState extends State<_General> { bool root_dir_exists = map['root_dir_exists']!; bool user_dir_exists = map['user_dir_exists']!; return _Card(title: 'Recording', children: [ - _OptionCheckBox(context, 'Automatically record incoming sessions', - kOptionAllowAutoRecordIncoming), - if (showRootDir) + if (!bind.isOutgoingOnly()) + _OptionCheckBox(context, 'Automatically record incoming sessions', + kOptionAllowAutoRecordIncoming), + if (!bind.isIncomingOnly()) + _OptionCheckBox(context, 'Automatically record outgoing sessions', + kOptionAllowAutoRecordOutgoing), + if (showRootDir && !bind.isOutgoingOnly()) Row( children: [ - Text('${translate("Incoming")}:'), + Text( + '${translate(bind.isIncomingOnly() ? "Directory" : "Incoming")}:'), Expanded( child: GestureDetector( onTap: root_dir_exists @@ -597,45 +602,49 @@ class _GeneralState extends State<_General> { ), ], ).marginOnly(left: _kContentHMargin), - Row( - children: [ - Text('${translate(showRootDir ? "Outgoing" : "Directory")}:'), - Expanded( - child: GestureDetector( - onTap: user_dir_exists - ? () => launchUrl(Uri.file(user_dir)) - : null, - child: Text( - user_dir, - softWrap: true, - style: user_dir_exists - ? const TextStyle(decoration: TextDecoration.underline) + if (!(showRootDir && bind.isIncomingOnly())) + Row( + children: [ + Text( + '${translate((showRootDir && !bind.isOutgoingOnly()) ? "Outgoing" : "Directory")}:'), + Expanded( + child: GestureDetector( + onTap: user_dir_exists + ? () => launchUrl(Uri.file(user_dir)) : null, - )).marginOnly(left: 10), - ), - ElevatedButton( - onPressed: isOptionFixed(kOptionVideoSaveDirectory) - ? null - : () async { - String? initialDirectory; - if (await Directory.fromUri(Uri.directory(user_dir)) - .exists()) { - initialDirectory = user_dir; - } - String? selectedDirectory = - await FilePicker.platform.getDirectoryPath( - initialDirectory: initialDirectory); - if (selectedDirectory != null) { - await bind.mainSetOption( - key: kOptionVideoSaveDirectory, - value: selectedDirectory); - setState(() {}); - } - }, - child: Text(translate('Change'))) - .marginOnly(left: 5), - ], - ).marginOnly(left: _kContentHMargin), + child: Text( + user_dir, + softWrap: true, + style: user_dir_exists + ? const TextStyle( + decoration: TextDecoration.underline) + : null, + )).marginOnly(left: 10), + ), + ElevatedButton( + onPressed: isOptionFixed(kOptionVideoSaveDirectory) + ? null + : () async { + String? initialDirectory; + if (await Directory.fromUri( + Uri.directory(user_dir)) + .exists()) { + initialDirectory = user_dir; + } + String? selectedDirectory = + await FilePicker.platform.getDirectoryPath( + initialDirectory: initialDirectory); + if (selectedDirectory != null) { + await bind.mainSetOption( + key: kOptionVideoSaveDirectory, + value: selectedDirectory); + setState(() {}); + } + }, + child: Text(translate('Change'))) + .marginOnly(left: 5), + ], + ).marginOnly(left: _kContentHMargin), ]); }); } diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 4ef8157da19b..cca2074a242c 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -115,6 +115,8 @@ class _RemotePageState extends State _ffi.imageModel.addCallbackOnFirstImage((String peerId) { showKBLayoutTypeChooserIfNeeded( _ffi.ffiModel.pi.platform, _ffi.dialogManager); + _ffi.recordingModel + .updateStatus(bind.sessionGetIsRecording(sessionId: _ffi.sessionId)); }); _ffi.start( widget.id, @@ -253,7 +255,6 @@ class _RemotePageState extends State _ffi.dialogManager.hideMobileActionsOverlay(); _ffi.imageModel.disposeImage(); _ffi.cursorModel.disposeImages(); - _ffi.recordingModel.onClose(); _rawKeyFocusNode.dispose(); await _ffi.close(closeSession: closeSession); _timer?.cancel(); diff --git a/flutter/lib/desktop/widgets/remote_toolbar.dart b/flutter/lib/desktop/widgets/remote_toolbar.dart index 3d8ca5e13149..75791ad093cc 100644 --- a/flutter/lib/desktop/widgets/remote_toolbar.dart +++ b/flutter/lib/desktop/widgets/remote_toolbar.dart @@ -1924,8 +1924,7 @@ class _RecordMenu extends StatelessWidget { var ffi = Provider.of(context); var recordingModel = Provider.of(context); final visible = - (recordingModel.start || ffi.permissions['recording'] != false) && - ffi.pi.currentDisplay != kAllDisplayValue; + (recordingModel.start || ffi.permissions['recording'] != false); if (!visible) return Offstage(); return _IconMenuButton( assetName: 'assets/rec.svg', diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index 395ce333365b..40890f228e6b 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -92,6 +92,13 @@ class _RemotePageState extends State { gFFI.chatModel .changeCurrentKey(MessageKey(widget.id, ChatModel.clientModeID)); _blockableOverlayState.applyFfi(gFFI); + gFFI.imageModel.addCallbackOnFirstImage((String peerId) { + gFFI.recordingModel + .updateStatus(bind.sessionGetIsRecording(sessionId: gFFI.sessionId)); + if (gFFI.recordingModel.start) { + showToast(translate('Automatically record outgoing sessions')); + } + }); } @override @@ -207,7 +214,7 @@ class _RemotePageState extends State { } void _handleNonIOSSoftKeyboardInput(String newValue) { - _composingTimer?.cancel(); + _composingTimer?.cancel(); if (_textController.value.isComposingRangeValid) { _composingTimer = Timer(Duration(milliseconds: 25), () { _handleNonIOSSoftKeyboardInput(_textController.value.text); diff --git a/flutter/lib/mobile/pages/settings_page.dart b/flutter/lib/mobile/pages/settings_page.dart index 8fac2ea2a040..e70ee5b35c7a 100644 --- a/flutter/lib/mobile/pages/settings_page.dart +++ b/flutter/lib/mobile/pages/settings_page.dart @@ -79,6 +79,7 @@ class _SettingsState extends State with WidgetsBindingObserver { var _enableRecordSession = false; var _enableHardwareCodec = false; var _autoRecordIncomingSession = false; + var _autoRecordOutgoingSession = false; var _allowAutoDisconnect = false; var _localIP = ""; var _directAccessPort = ""; @@ -104,6 +105,8 @@ class _SettingsState extends State with WidgetsBindingObserver { bind.mainGetOptionSync(key: kOptionEnableHwcodec)); _autoRecordIncomingSession = option2bool(kOptionAllowAutoRecordIncoming, bind.mainGetOptionSync(key: kOptionAllowAutoRecordIncoming)); + _autoRecordOutgoingSession = option2bool(kOptionAllowAutoRecordOutgoing, + bind.mainGetOptionSync(key: kOptionAllowAutoRecordOutgoing)); _localIP = bind.mainGetOptionSync(key: 'local-ip-addr'); _directAccessPort = bind.mainGetOptionSync(key: kOptionDirectAccessPort); _allowAutoDisconnect = option2bool(kOptionAllowAutoDisconnect, @@ -231,6 +234,7 @@ class _SettingsState extends State with WidgetsBindingObserver { Widget build(BuildContext context) { Provider.of(context); final outgoingOnly = bind.isOutgoingOnly(); + final incommingOnly = bind.isIncomingOnly(); final customClientSection = CustomSettingsSection( child: Column( children: [ @@ -674,32 +678,55 @@ class _SettingsState extends State with WidgetsBindingObserver { }, ), ]), - if (isAndroid && !outgoingOnly) + if (isAndroid) SettingsSection( title: Text(translate("Recording")), tiles: [ - SettingsTile.switchTile( - title: - Text(translate('Automatically record incoming sessions')), - leading: Icon(Icons.videocam), - description: Text( - "${translate("Directory")}: ${bind.mainVideoSaveDirectory(root: false)}"), - initialValue: _autoRecordIncomingSession, - onToggle: isOptionFixed(kOptionAllowAutoRecordIncoming) - ? null - : (v) async { - await bind.mainSetOption( - key: kOptionAllowAutoRecordIncoming, - value: - bool2option(kOptionAllowAutoRecordIncoming, v)); - final newValue = option2bool( - kOptionAllowAutoRecordIncoming, - await bind.mainGetOption( - key: kOptionAllowAutoRecordIncoming)); - setState(() { - _autoRecordIncomingSession = newValue; - }); - }, + if (!outgoingOnly) + SettingsTile.switchTile( + title: + Text(translate('Automatically record incoming sessions')), + initialValue: _autoRecordIncomingSession, + onToggle: isOptionFixed(kOptionAllowAutoRecordIncoming) + ? null + : (v) async { + await bind.mainSetOption( + key: kOptionAllowAutoRecordIncoming, + value: bool2option( + kOptionAllowAutoRecordIncoming, v)); + final newValue = option2bool( + kOptionAllowAutoRecordIncoming, + await bind.mainGetOption( + key: kOptionAllowAutoRecordIncoming)); + setState(() { + _autoRecordIncomingSession = newValue; + }); + }, + ), + if (!incommingOnly) + SettingsTile.switchTile( + title: + Text(translate('Automatically record outgoing sessions')), + initialValue: _autoRecordOutgoingSession, + onToggle: isOptionFixed(kOptionAllowAutoRecordOutgoing) + ? null + : (v) async { + await bind.mainSetOption( + key: kOptionAllowAutoRecordOutgoing, + value: bool2option( + kOptionAllowAutoRecordOutgoing, v)); + final newValue = option2bool( + kOptionAllowAutoRecordOutgoing, + await bind.mainGetOption( + key: kOptionAllowAutoRecordOutgoing)); + setState(() { + _autoRecordOutgoingSession = newValue; + }); + }, + ), + SettingsTile( + title: Text(translate("Directory")), + description: Text(bind.mainVideoSaveDirectory(root: false)), ), ], ), diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index d836f62a6e8a..f0e4cd75f9e7 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -397,6 +397,10 @@ class FfiModel with ChangeNotifier { if (isWeb) { parent.target?.fileModel.onSelectedFiles(evt); } + } else if (name == "record_status") { + if (desktopType == DesktopType.remote || isMobile) { + parent.target?.recordingModel.updateStatus(evt['start'] == 'true'); + } } else { debugPrint('Event is not handled in the fixed branch: $name'); } @@ -527,7 +531,6 @@ class FfiModel with ChangeNotifier { } } - parent.target?.recordingModel.onSwitchDisplay(); if (!_pi.isSupportMultiUiSession || _pi.currentDisplay == display) { handleResolutions(peerId, evt['resolutions']); } @@ -1135,8 +1138,6 @@ class FfiModel with ChangeNotifier { // Directly switch to the new display without waiting for the response. switchToNewDisplay(int display, SessionID sessionId, String peerId, {bool updateCursorPos = false}) { - // VideoHandler creation is upon when video frames are received, so either caching commands(don't know next width/height) or stopping recording when switching displays. - parent.target?.recordingModel.onClose(); // no need to wait for the response pi.currentDisplay = display; updateCurDisplay(sessionId, updateCursorPos: updateCursorPos); @@ -2342,25 +2343,7 @@ class RecordingModel with ChangeNotifier { WeakReference parent; RecordingModel(this.parent); bool _start = false; - get start => _start; - - onSwitchDisplay() { - if (isIOS || !_start) return; - final sessionId = parent.target?.sessionId; - int? width = parent.target?.canvasModel.getDisplayWidth(); - int? height = parent.target?.canvasModel.getDisplayHeight(); - if (sessionId == null || width == null || height == null) return; - final pi = parent.target?.ffiModel.pi; - if (pi == null) return; - final currentDisplay = pi.currentDisplay; - if (currentDisplay == kAllDisplayValue) return; - bind.sessionRecordScreen( - sessionId: sessionId, - start: true, - display: currentDisplay, - width: width, - height: height); - } + bool get start => _start; toggle() async { if (isIOS) return; @@ -2368,48 +2351,16 @@ class RecordingModel with ChangeNotifier { if (sessionId == null) return; final pi = parent.target?.ffiModel.pi; if (pi == null) return; - final currentDisplay = pi.currentDisplay; - if (currentDisplay == kAllDisplayValue) return; - _start = !_start; - notifyListeners(); - await _sendStatusMessage(sessionId, pi, _start); - if (_start) { - sessionRefreshVideo(sessionId, pi); - if (versionCmp(pi.version, '1.2.4') >= 0) { - // will not receive SwitchDisplay since 1.2.4 - onSwitchDisplay(); - } - } else { - bind.sessionRecordScreen( - sessionId: sessionId, - start: false, - display: currentDisplay, - width: 0, - height: 0); + bool value = !_start; + if (value) { + await sessionRefreshVideo(sessionId, pi); } + await bind.sessionRecordScreen(sessionId: sessionId, start: value); } - onClose() async { - if (isIOS) return; - final sessionId = parent.target?.sessionId; - if (sessionId == null) return; - if (!_start) return; - _start = false; - final pi = parent.target?.ffiModel.pi; - if (pi == null) return; - final currentDisplay = pi.currentDisplay; - if (currentDisplay == kAllDisplayValue) return; - await _sendStatusMessage(sessionId, pi, false); - bind.sessionRecordScreen( - sessionId: sessionId, - start: false, - display: currentDisplay, - width: 0, - height: 0); - } - - _sendStatusMessage(SessionID sessionId, PeerInfo pi, bool status) async { - await bind.sessionRecordStatus(sessionId: sessionId, status: status); + updateStatus(bool status) { + _start = status; + notifyListeners(); } } diff --git a/libs/hbb_common/src/config.rs b/libs/hbb_common/src/config.rs index f0f7ec7317ec..94f4ec9d988d 100644 --- a/libs/hbb_common/src/config.rs +++ b/libs/hbb_common/src/config.rs @@ -965,6 +965,10 @@ impl Config { .unwrap_or_default() } + pub fn get_bool_option(k: &str) -> bool { + option2bool(k, &Self::get_option(k)) + } + pub fn set_option(k: String, v: String) { if !is_option_can_save(&OVERWRITE_SETTINGS, &k, &DEFAULT_SETTINGS, &v) { return; @@ -2198,6 +2202,7 @@ pub mod keys { pub const OPTION_AUTO_DISCONNECT_TIMEOUT: &str = "auto-disconnect-timeout"; pub const OPTION_ALLOW_ONLY_CONN_WINDOW_OPEN: &str = "allow-only-conn-window-open"; pub const OPTION_ALLOW_AUTO_RECORD_INCOMING: &str = "allow-auto-record-incoming"; + pub const OPTION_ALLOW_AUTO_RECORD_OUTGOING: &str = "allow-auto-record-outgoing"; pub const OPTION_VIDEO_SAVE_DIRECTORY: &str = "video-save-directory"; pub const OPTION_ENABLE_ABR: &str = "enable-abr"; pub const OPTION_ALLOW_REMOVE_WALLPAPER: &str = "allow-remove-wallpaper"; @@ -2342,6 +2347,7 @@ pub mod keys { OPTION_AUTO_DISCONNECT_TIMEOUT, OPTION_ALLOW_ONLY_CONN_WINDOW_OPEN, OPTION_ALLOW_AUTO_RECORD_INCOMING, + OPTION_ALLOW_AUTO_RECORD_OUTGOING, OPTION_VIDEO_SAVE_DIRECTORY, OPTION_ENABLE_ABR, OPTION_ALLOW_REMOVE_WALLPAPER, diff --git a/libs/scrap/Cargo.toml b/libs/scrap/Cargo.toml index 3a0784e7cca4..529010f16072 100644 --- a/libs/scrap/Cargo.toml +++ b/libs/scrap/Cargo.toml @@ -62,4 +62,3 @@ gstreamer-video = { version = "0.16", optional = true } git = "https://github.com/rustdesk-org/hwcodec" optional = true - diff --git a/libs/scrap/src/common/codec.rs b/libs/scrap/src/common/codec.rs index 07ff0f91d243..dad924c862cd 100644 --- a/libs/scrap/src/common/codec.rs +++ b/libs/scrap/src/common/codec.rs @@ -15,7 +15,7 @@ use crate::{ aom::{self, AomDecoder, AomEncoder, AomEncoderConfig}, common::GoogleImage, vpxcodec::{self, VpxDecoder, VpxDecoderConfig, VpxEncoder, VpxEncoderConfig, VpxVideoCodecId}, - CodecFormat, EncodeInput, EncodeYuvFormat, ImageRgb, + CodecFormat, EncodeInput, EncodeYuvFormat, ImageRgb, ImageTexture, }; use hbb_common::{ @@ -623,7 +623,7 @@ impl Decoder { &mut self, frame: &video_frame::Union, rgb: &mut ImageRgb, - _texture: &mut *mut c_void, + _texture: &mut ImageTexture, _pixelbuffer: &mut bool, chroma: &mut Option, ) -> ResultType { @@ -777,12 +777,16 @@ impl Decoder { fn handle_vram_video_frame( decoder: &mut VRamDecoder, frames: &EncodedVideoFrames, - texture: &mut *mut c_void, + texture: &mut ImageTexture, ) -> ResultType { let mut ret = false; for h26x in frames.frames.iter() { for image in decoder.decode(&h26x.data)? { - *texture = image.frame.texture; + *texture = ImageTexture { + texture: image.frame.texture, + w: image.frame.width as _, + h: image.frame.height as _, + }; ret = true; } } diff --git a/libs/scrap/src/common/mod.rs b/libs/scrap/src/common/mod.rs index 635f0ec26d45..ee96f57c8514 100644 --- a/libs/scrap/src/common/mod.rs +++ b/libs/scrap/src/common/mod.rs @@ -96,6 +96,22 @@ impl ImageRgb { } } +pub struct ImageTexture { + pub texture: *mut c_void, + pub w: usize, + pub h: usize, +} + +impl Default for ImageTexture { + fn default() -> Self { + Self { + texture: std::ptr::null_mut(), + w: 0, + h: 0, + } + } +} + #[inline] pub fn would_block_if_equal(old: &mut Vec, b: &[u8]) -> std::io::Result<()> { // does this really help? @@ -296,6 +312,19 @@ impl From<&VideoFrame> for CodecFormat { } } +impl From<&video_frame::Union> for CodecFormat { + fn from(it: &video_frame::Union) -> Self { + match it { + video_frame::Union::Vp8s(_) => CodecFormat::VP8, + video_frame::Union::Vp9s(_) => CodecFormat::VP9, + video_frame::Union::Av1s(_) => CodecFormat::AV1, + video_frame::Union::H264s(_) => CodecFormat::H264, + video_frame::Union::H265s(_) => CodecFormat::H265, + _ => CodecFormat::Unknown, + } + } +} + impl From<&CodecName> for CodecFormat { fn from(value: &CodecName) -> Self { match value { diff --git a/libs/scrap/src/common/record.rs b/libs/scrap/src/common/record.rs index 52973c2b244b..c53b7743147f 100644 --- a/libs/scrap/src/common/record.rs +++ b/libs/scrap/src/common/record.rs @@ -25,22 +25,28 @@ pub struct RecorderContext { pub server: bool, pub id: String, pub dir: String, + pub display: usize, + pub tx: Option>, +} + +#[derive(Debug, Clone)] +pub struct RecorderContext2 { pub filename: String, pub width: usize, pub height: usize, pub format: CodecFormat, - pub tx: Option>, } -impl RecorderContext { - pub fn set_filename(&mut self) -> ResultType<()> { - if !PathBuf::from(&self.dir).exists() { - std::fs::create_dir_all(&self.dir)?; +impl RecorderContext2 { + pub fn set_filename(&mut self, ctx: &RecorderContext) -> ResultType<()> { + if !PathBuf::from(&ctx.dir).exists() { + std::fs::create_dir_all(&ctx.dir)?; } - let file = if self.server { "incoming" } else { "outgoing" }.to_string() + let file = if ctx.server { "incoming" } else { "outgoing" }.to_string() + "_" - + &self.id.clone() + + &ctx.id.clone() + &chrono::Local::now().format("_%Y%m%d%H%M%S%3f_").to_string() + + &format!("display{}_", ctx.display) + &self.format.to_string().to_lowercase() + if self.format == CodecFormat::VP9 || self.format == CodecFormat::VP8 @@ -50,11 +56,10 @@ impl RecorderContext { } else { ".mp4" }; - self.filename = PathBuf::from(&self.dir) + self.filename = PathBuf::from(&ctx.dir) .join(file) .to_string_lossy() .to_string(); - log::info!("video will save to {}", self.filename); Ok(()) } } @@ -63,7 +68,7 @@ unsafe impl Send for Recorder {} unsafe impl Sync for Recorder {} pub trait RecorderApi { - fn new(ctx: RecorderContext) -> ResultType + fn new(ctx: RecorderContext, ctx2: RecorderContext2) -> ResultType where Self: Sized; fn write_video(&mut self, frame: &EncodedVideoFrame) -> bool; @@ -78,13 +83,15 @@ pub enum RecordState { } pub struct Recorder { - pub inner: Box, + pub inner: Option>, ctx: RecorderContext, + ctx2: Option, pts: Option, + check_failed: bool, } impl Deref for Recorder { - type Target = Box; + type Target = Option>; fn deref(&self) -> &Self::Target { &self.inner @@ -98,114 +105,123 @@ impl DerefMut for Recorder { } impl Recorder { - pub fn new(mut ctx: RecorderContext) -> ResultType { - ctx.set_filename()?; - let recorder = match ctx.format { - CodecFormat::VP8 | CodecFormat::VP9 | CodecFormat::AV1 => Recorder { - inner: Box::new(WebmRecorder::new(ctx.clone())?), - ctx, - pts: None, - }, - #[cfg(feature = "hwcodec")] - _ => Recorder { - inner: Box::new(HwRecorder::new(ctx.clone())?), - ctx, - pts: None, - }, - #[cfg(not(feature = "hwcodec"))] - _ => bail!("unsupported codec type"), - }; - recorder.send_state(RecordState::NewFile(recorder.ctx.filename.clone())); - Ok(recorder) + pub fn new(ctx: RecorderContext) -> ResultType { + Ok(Self { + inner: None, + ctx, + ctx2: None, + pts: None, + check_failed: false, + }) } - fn change(&mut self, mut ctx: RecorderContext) -> ResultType<()> { - ctx.set_filename()?; - self.inner = match ctx.format { - CodecFormat::VP8 | CodecFormat::VP9 | CodecFormat::AV1 => { - Box::new(WebmRecorder::new(ctx.clone())?) + fn check(&mut self, w: usize, h: usize, format: CodecFormat) -> ResultType<()> { + match self.ctx2 { + Some(ref ctx2) => { + if ctx2.width != w || ctx2.height != h || ctx2.format != format { + let mut ctx2 = RecorderContext2 { + width: w, + height: h, + format, + filename: Default::default(), + }; + ctx2.set_filename(&self.ctx)?; + self.ctx2 = Some(ctx2); + self.inner = None; + } } - #[cfg(feature = "hwcodec")] - _ => Box::new(HwRecorder::new(ctx.clone())?), - #[cfg(not(feature = "hwcodec"))] - _ => bail!("unsupported codec type"), + None => { + let mut ctx2 = RecorderContext2 { + width: w, + height: h, + format, + filename: Default::default(), + }; + ctx2.set_filename(&self.ctx)?; + self.ctx2 = Some(ctx2); + self.inner = None; + } + } + let Some(ctx2) = &self.ctx2 else { + bail!("ctx2 is None"); }; - self.ctx = ctx; - self.pts = None; - self.send_state(RecordState::NewFile(self.ctx.filename.clone())); + if self.inner.is_none() { + self.inner = match format { + CodecFormat::VP8 | CodecFormat::VP9 | CodecFormat::AV1 => Some(Box::new( + WebmRecorder::new(self.ctx.clone(), (*ctx2).clone())?, + )), + #[cfg(feature = "hwcodec")] + _ => Some(Box::new(HwRecorder::new( + self.ctx.clone(), + (*ctx2).clone(), + )?)), + #[cfg(not(feature = "hwcodec"))] + _ => bail!("unsupported codec type"), + }; + self.pts = None; + self.send_state(RecordState::NewFile(ctx2.filename.clone())); + } Ok(()) } - pub fn write_message(&mut self, msg: &Message) { + pub fn write_message(&mut self, msg: &Message, w: usize, h: usize) { if let Some(message::Union::VideoFrame(vf)) = &msg.union { if let Some(frame) = &vf.union { - self.write_frame(frame).ok(); + self.write_frame(frame, w, h).ok(); } } } - pub fn write_frame(&mut self, frame: &video_frame::Union) -> ResultType<()> { + pub fn write_frame( + &mut self, + frame: &video_frame::Union, + w: usize, + h: usize, + ) -> ResultType<()> { + if self.check_failed { + bail!("check failed"); + } + let format = CodecFormat::from(frame); + if format == CodecFormat::Unknown { + bail!("unsupported frame type"); + } + let res = self.check(w, h, format); + if res.is_err() { + self.check_failed = true; + log::error!("check failed: {:?}", res); + res?; + } match frame { video_frame::Union::Vp8s(vp8s) => { - if self.ctx.format != CodecFormat::VP8 { - self.change(RecorderContext { - format: CodecFormat::VP8, - ..self.ctx.clone() - })?; - } for f in vp8s.frames.iter() { - self.check_pts(f.pts)?; - self.write_video(f); + self.check_pts(f.pts, w, h, format)?; + self.as_mut().map(|x| x.write_video(f)); } } video_frame::Union::Vp9s(vp9s) => { - if self.ctx.format != CodecFormat::VP9 { - self.change(RecorderContext { - format: CodecFormat::VP9, - ..self.ctx.clone() - })?; - } for f in vp9s.frames.iter() { - self.check_pts(f.pts)?; - self.write_video(f); + self.check_pts(f.pts, w, h, format)?; + self.as_mut().map(|x| x.write_video(f)); } } video_frame::Union::Av1s(av1s) => { - if self.ctx.format != CodecFormat::AV1 { - self.change(RecorderContext { - format: CodecFormat::AV1, - ..self.ctx.clone() - })?; - } for f in av1s.frames.iter() { - self.check_pts(f.pts)?; - self.write_video(f); + self.check_pts(f.pts, w, h, format)?; + self.as_mut().map(|x| x.write_video(f)); } } #[cfg(feature = "hwcodec")] video_frame::Union::H264s(h264s) => { - if self.ctx.format != CodecFormat::H264 { - self.change(RecorderContext { - format: CodecFormat::H264, - ..self.ctx.clone() - })?; - } for f in h264s.frames.iter() { - self.check_pts(f.pts)?; - self.write_video(f); + self.check_pts(f.pts, w, h, format)?; + self.as_mut().map(|x| x.write_video(f)); } } #[cfg(feature = "hwcodec")] video_frame::Union::H265s(h265s) => { - if self.ctx.format != CodecFormat::H265 { - self.change(RecorderContext { - format: CodecFormat::H265, - ..self.ctx.clone() - })?; - } for f in h265s.frames.iter() { - self.check_pts(f.pts)?; - self.write_video(f); + self.check_pts(f.pts, w, h, format)?; + self.as_mut().map(|x| x.write_video(f)); } } _ => bail!("unsupported frame type"), @@ -214,13 +230,21 @@ impl Recorder { Ok(()) } - fn check_pts(&mut self, pts: i64) -> ResultType<()> { + fn check_pts(&mut self, pts: i64, w: usize, h: usize, format: CodecFormat) -> ResultType<()> { // https://stackoverflow.com/questions/76379101/how-to-create-one-playable-webm-file-from-two-different-video-tracks-with-same-c let old_pts = self.pts; self.pts = Some(pts); if old_pts.clone().unwrap_or_default() > pts { log::info!("pts {:?} -> {}, change record filename", old_pts, pts); - self.change(self.ctx.clone())?; + self.inner = None; + self.ctx2 = None; + let res = self.check(w, h, format); + if res.is_err() { + self.check_failed = true; + log::error!("check failed: {:?}", res); + res?; + } + self.pts = Some(pts); } Ok(()) } @@ -234,21 +258,22 @@ struct WebmRecorder { vt: VideoTrack, webm: Option>>, ctx: RecorderContext, + ctx2: RecorderContext2, key: bool, written: bool, start: Instant, } impl RecorderApi for WebmRecorder { - fn new(ctx: RecorderContext) -> ResultType { + fn new(ctx: RecorderContext, ctx2: RecorderContext2) -> ResultType { let out = match { OpenOptions::new() .write(true) .create_new(true) - .open(&ctx.filename) + .open(&ctx2.filename) } { Ok(file) => file, - Err(ref e) if e.kind() == io::ErrorKind::AlreadyExists => File::create(&ctx.filename)?, + Err(ref e) if e.kind() == io::ErrorKind::AlreadyExists => File::create(&ctx2.filename)?, Err(e) => return Err(e.into()), }; let mut webm = match mux::Segment::new(mux::Writer::new(out)) { @@ -256,18 +281,18 @@ impl RecorderApi for WebmRecorder { None => bail!("Failed to create webm mux"), }; let vt = webm.add_video_track( - ctx.width as _, - ctx.height as _, + ctx2.width as _, + ctx2.height as _, None, - if ctx.format == CodecFormat::VP9 { + if ctx2.format == CodecFormat::VP9 { mux::VideoCodecId::VP9 - } else if ctx.format == CodecFormat::VP8 { + } else if ctx2.format == CodecFormat::VP8 { mux::VideoCodecId::VP8 } else { mux::VideoCodecId::AV1 }, ); - if ctx.format == CodecFormat::AV1 { + if ctx2.format == CodecFormat::AV1 { // [129, 8, 12, 0] in 3.6.0, but zero works let codec_private = vec![0, 0, 0, 0]; if !webm.set_codec_private(vt.track_number(), &codec_private) { @@ -278,6 +303,7 @@ impl RecorderApi for WebmRecorder { vt, webm: Some(webm), ctx, + ctx2, key: false, written: false, start: Instant::now(), @@ -307,7 +333,7 @@ impl Drop for WebmRecorder { let _ = std::mem::replace(&mut self.webm, None).map_or(false, |webm| webm.finalize(None)); let mut state = RecordState::WriteTail; if !self.written || self.start.elapsed().as_secs() < MIN_SECS { - std::fs::remove_file(&self.ctx.filename).ok(); + std::fs::remove_file(&self.ctx2.filename).ok(); state = RecordState::RemoveFile; } self.ctx.tx.as_ref().map(|tx| tx.send(state)); @@ -318,6 +344,7 @@ impl Drop for WebmRecorder { struct HwRecorder { muxer: Muxer, ctx: RecorderContext, + ctx2: RecorderContext2, written: bool, key: bool, start: Instant, @@ -325,18 +352,19 @@ struct HwRecorder { #[cfg(feature = "hwcodec")] impl RecorderApi for HwRecorder { - fn new(ctx: RecorderContext) -> ResultType { + fn new(ctx: RecorderContext, ctx2: RecorderContext2) -> ResultType { let muxer = Muxer::new(MuxContext { - filename: ctx.filename.clone(), - width: ctx.width, - height: ctx.height, - is265: ctx.format == CodecFormat::H265, + filename: ctx2.filename.clone(), + width: ctx2.width, + height: ctx2.height, + is265: ctx2.format == CodecFormat::H265, framerate: crate::hwcodec::DEFAULT_FPS as _, }) .map_err(|_| anyhow!("Failed to create hardware muxer"))?; Ok(HwRecorder { muxer, ctx, + ctx2, written: false, key: false, start: Instant::now(), @@ -365,7 +393,7 @@ impl Drop for HwRecorder { self.muxer.write_tail().ok(); let mut state = RecordState::WriteTail; if !self.written || self.start.elapsed().as_secs() < MIN_SECS { - std::fs::remove_file(&self.ctx.filename).ok(); + std::fs::remove_file(&self.ctx2.filename).ok(); state = RecordState::RemoveFile; } self.ctx.tx.as_ref().map(|tx| tx.send(state)); diff --git a/src/client.rs b/src/client.rs index 9e49b84e2e27..a1b2b83d4366 100644 --- a/src/client.rs +++ b/src/client.rs @@ -30,7 +30,6 @@ pub use file_trait::FileManager; #[cfg(not(feature = "flutter"))] #[cfg(not(any(target_os = "android", target_os = "ios")))] use hbb_common::tokio::sync::mpsc::UnboundedSender; -use hbb_common::tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver}; use hbb_common::{ allow_err, anyhow::{anyhow, Context}, @@ -54,11 +53,15 @@ use hbb_common::{ }, AddrMangle, ResultType, Stream, }; +use hbb_common::{ + config::keys::OPTION_ALLOW_AUTO_RECORD_OUTGOING, + tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver}, +}; pub use helper::*; use scrap::{ codec::Decoder, record::{Recorder, RecorderContext}, - CodecFormat, ImageFormat, ImageRgb, + CodecFormat, ImageFormat, ImageRgb, ImageTexture, }; use crate::{ @@ -1146,7 +1149,7 @@ impl AudioHandler { pub struct VideoHandler { decoder: Decoder, pub rgb: ImageRgb, - pub texture: *mut c_void, + pub texture: ImageTexture, recorder: Arc>>, record: bool, _display: usize, // useful for debug @@ -1172,7 +1175,7 @@ impl VideoHandler { VideoHandler { decoder: Decoder::new(format, luid), rgb: ImageRgb::new(ImageFormat::ARGB, crate::get_dst_align_rgba()), - texture: std::ptr::null_mut(), + texture: Default::default(), recorder: Default::default(), record: false, _display, @@ -1220,11 +1223,14 @@ impl VideoHandler { } self.first_frame = false; if self.record { - self.recorder - .lock() - .unwrap() - .as_mut() - .map(|r| r.write_frame(frame)); + self.recorder.lock().unwrap().as_mut().map(|r| { + let (w, h) = if *pixelbuffer { + (self.rgb.w, self.rgb.h) + } else { + (self.texture.w, self.texture.h) + }; + r.write_frame(frame, w, h).ok(); + }); } res } @@ -1248,17 +1254,14 @@ impl VideoHandler { } /// Start or stop screen record. - pub fn record_screen(&mut self, start: bool, w: i32, h: i32, id: String) { + pub fn record_screen(&mut self, start: bool, id: String, display: usize) { self.record = false; if start { self.recorder = Recorder::new(RecorderContext { server: false, id, dir: crate::ui_interface::video_save_directory(false), - filename: "".to_owned(), - width: w as _, - height: h as _, - format: scrap::CodecFormat::VP9, + display, tx: None, }) .map_or(Default::default(), |r| Arc::new(Mutex::new(Some(r)))); @@ -1347,6 +1350,7 @@ pub struct LoginConfigHandler { password_source: PasswordSource, // where the sent password comes from shared_password: Option, // Store the shared password pub enable_trusted_devices: bool, + pub record: bool, } impl Deref for LoginConfigHandler { @@ -1438,6 +1442,7 @@ impl LoginConfigHandler { self.adapter_luid = adapter_luid; self.selected_windows_session_id = None; self.shared_password = shared_password; + self.record = Config::get_bool_option(OPTION_ALLOW_AUTO_RECORD_OUTGOING); } /// Check if the client should auto login. @@ -2227,7 +2232,7 @@ pub enum MediaData { AudioFrame(Box), AudioFormat(AudioFormat), Reset(Option), - RecordScreen(bool, usize, i32, i32, String), + RecordScreen(bool), } pub type MediaSender = mpsc::Sender; @@ -2303,10 +2308,16 @@ where let start = std::time::Instant::now(); let format = CodecFormat::from(&vf); if !handler_controller_map.contains_key(&display) { + let mut handler = VideoHandler::new(format, display); + let record = session.lc.read().unwrap().record; + let id = session.lc.read().unwrap().id.clone(); + if record { + handler.record_screen(record, id, display); + } handler_controller_map.insert( display, VideoHandlerController { - handler: VideoHandler::new(format, display), + handler, skip_beginning: 0, }, ); @@ -2325,7 +2336,7 @@ where video_callback( display, &mut handler_controller.handler.rgb, - handler_controller.handler.texture, + handler_controller.handler.texture.texture, pixelbuffer, ); @@ -2399,18 +2410,19 @@ where } } } - MediaData::RecordScreen(start, display, w, h, id) => { - log::info!("record screen command: start: {start}, display: {display}"); - // Compatible with the sciter version(single ui session). - // For the sciter version, there're no multi-ui-sessions for one connection. - // The display is always 0, video_handler_controllers.len() is always 1. So we use the first video handler. - if let Some(handler_controler) = handler_controller_map.get_mut(&display) { - handler_controler.handler.record_screen(start, w, h, id); - } else if handler_controller_map.len() == 1 { - if let Some(handler_controler) = - handler_controller_map.values_mut().next() - { - handler_controler.handler.record_screen(start, w, h, id); + MediaData::RecordScreen(start) => { + log::info!("record screen command: start: {start}"); + let record = session.lc.read().unwrap().record; + session.update_record_status(start); + if record != start { + session.lc.write().unwrap().record = start; + let id = session.lc.read().unwrap().id.clone(); + for (display, handler_controler) in handler_controller_map.iter_mut() { + handler_controler.handler.record_screen( + start, + id.clone(), + *display, + ); } } } @@ -3169,7 +3181,7 @@ pub enum Data { SetConfirmOverrideFile((i32, i32, bool, bool, bool)), AddJob((i32, String, String, i32, bool, bool)), ResumeJob((i32, bool)), - RecordScreen(bool, usize, i32, i32, String), + RecordScreen(bool), ElevateDirect, ElevateWithLogon(String, String), NewVoiceCall, diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index 84d8a897cf58..cc74c96edd14 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -837,10 +837,8 @@ impl Remote { self.handle_job_status(id, -1, err); } } - Data::RecordScreen(start, display, w, h, id) => { - let _ = self - .video_sender - .send(MediaData::RecordScreen(start, display, w, h, id)); + Data::RecordScreen(start) => { + let _ = self.video_sender.send(MediaData::RecordScreen(start)); } Data::ElevateDirect => { let mut request = ElevationRequest::new(); @@ -1218,7 +1216,7 @@ impl Remote { crate::plugin::handle_listen_event( crate::plugin::EVENT_ON_CONN_CLIENT.to_owned(), self.handler.get_id(), - ) + ); } if self.handler.is_file_transfer() { diff --git a/src/flutter.rs b/src/flutter.rs index cbeb3e2c3d3a..69266f51c1ce 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -17,7 +17,7 @@ use serde::Serialize; use serde_json::json; use std::{ - collections::HashMap, + collections::{HashMap, HashSet}, ffi::CString, os::raw::{c_char, c_int, c_void}, str::FromStr, @@ -1010,6 +1010,10 @@ impl InvokeUiSession for FlutterHandler { rgba_data.valid = false; } } + + fn update_record_status(&self, start: bool) { + self.push_event("record_status", &[("start", &start.to_string())], &[]); + } } impl FlutterHandler { @@ -1830,7 +1834,6 @@ pub(super) fn session_update_virtual_display(session: &FlutterSession, index: i3 // sessions mod is used to avoid the big lock of sessions' map. pub mod sessions { - use std::collections::HashSet; use super::*; diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index d029de1d2560..979e25e8d267 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -241,21 +241,17 @@ pub fn session_is_multi_ui_session(session_id: SessionID) -> SyncReturn { } } -pub fn session_record_screen( - session_id: SessionID, - start: bool, - display: usize, - width: usize, - height: usize, -) { +pub fn session_record_screen(session_id: SessionID, start: bool) { if let Some(session) = sessions::get_session_by_session_id(&session_id) { - session.record_screen(start, display as _, width as _, height as _); + session.record_screen(start); } } -pub fn session_record_status(session_id: SessionID, status: bool) { +pub fn session_get_is_recording(session_id: SessionID) -> SyncReturn { if let Some(session) = sessions::get_session_by_session_id(&session_id) { - session.record_status(status); + SyncReturn(session.is_recording()) + } else { + SyncReturn(false) } } diff --git a/src/lang/ar.rs b/src/lang/ar.rs index 6cae020b6e45..fe7e853815b6 100644 --- a/src/lang/ar.rs +++ b/src/lang/ar.rs @@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recording", "التسجيل"), ("Directory", "المسار"), ("Automatically record incoming sessions", "تسجيل الجلسات القادمة تلقائيا"), + ("Automatically record outgoing sessions", ""), ("Change", "تغيير"), ("Start session recording", "بدء تسجيل الجلسة"), ("Stop session recording", "ايقاف تسجيل الجلسة"), diff --git a/src/lang/be.rs b/src/lang/be.rs index 7e9f928ae573..da9be46401c3 100644 --- a/src/lang/be.rs +++ b/src/lang/be.rs @@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recording", "Запіс"), ("Directory", "Тэчка"), ("Automatically record incoming sessions", "Аўтаматычна запісваць уваходныя сесіі"), + ("Automatically record outgoing sessions", ""), ("Change", "Змяніць"), ("Start session recording", "Пачаць запіс сесіі"), ("Stop session recording", "Спыніць запіс сесіі"), diff --git a/src/lang/bg.rs b/src/lang/bg.rs index 9f6282e75234..72b5fcf19717 100644 --- a/src/lang/bg.rs +++ b/src/lang/bg.rs @@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recording", "Записване"), ("Directory", "Директория"), ("Automatically record incoming sessions", "Автоматичен запис на входящи сесии"), + ("Automatically record outgoing sessions", ""), ("Change", "Промяна"), ("Start session recording", "Започванена запис"), ("Stop session recording", "Край на запис"), diff --git a/src/lang/ca.rs b/src/lang/ca.rs index 64748c9ac093..fbe3cde5fcd5 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recording", "Gravació"), ("Directory", "Contactes"), ("Automatically record incoming sessions", "Enregistrament automàtic de sessions entrants"), + ("Automatically record outgoing sessions", ""), ("Change", "Canvia"), ("Start session recording", "Inicia la gravació de la sessió"), ("Stop session recording", "Atura la gravació de la sessió"), diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 5ad91ada84f7..f57d100d5e58 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -363,7 +363,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Unpin Toolbar", "取消固定工具栏"), ("Recording", "录屏"), ("Directory", "目录"), - ("Automatically record incoming sessions", "自动录制来访会话"), + ("Automatically record incoming sessions", "自动录制传入会话"), + ("Automatically record outgoing sessions", "自动录制传出会话"), ("Change", "更改"), ("Start session recording", "开始录屏"), ("Stop session recording", "结束录屏"), diff --git a/src/lang/cs.rs b/src/lang/cs.rs index 8db1470b98a5..d72b85a0cf50 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recording", "Nahrávání"), ("Directory", "Adresář"), ("Automatically record incoming sessions", "Automaticky nahrávat příchozí relace"), + ("Automatically record outgoing sessions", ""), ("Change", "Změnit"), ("Start session recording", "Spustit záznam relace"), ("Stop session recording", "Zastavit záznam relace"), diff --git a/src/lang/da.rs b/src/lang/da.rs index f867cac467a8..558b2fa45410 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recording", "Optager"), ("Directory", "Mappe"), ("Automatically record incoming sessions", "Optag automatisk indgående sessioner"), + ("Automatically record outgoing sessions", ""), ("Change", "Ændr"), ("Start session recording", "Start sessionsoptagelse"), ("Stop session recording", "Stop sessionsoptagelse"), diff --git a/src/lang/de.rs b/src/lang/de.rs index 4213c86de740..0a78ee2b55e8 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recording", "Aufnahme"), ("Directory", "Verzeichnis"), ("Automatically record incoming sessions", "Eingehende Sitzungen automatisch aufzeichnen"), + ("Automatically record outgoing sessions", ""), ("Change", "Ändern"), ("Start session recording", "Sitzungsaufzeichnung starten"), ("Stop session recording", "Sitzungsaufzeichnung beenden"), diff --git a/src/lang/el.rs b/src/lang/el.rs index 625867c343e9..9725ecc78864 100644 --- a/src/lang/el.rs +++ b/src/lang/el.rs @@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recording", "Εγγραφή"), ("Directory", "Φάκελος εγγραφών"), ("Automatically record incoming sessions", "Αυτόματη εγγραφή εισερχόμενων συνεδριών"), + ("Automatically record outgoing sessions", ""), ("Change", "Αλλαγή"), ("Start session recording", "Έναρξη εγγραφής συνεδρίας"), ("Stop session recording", "Διακοπή εγγραφής συνεδρίας"), diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 217b37a6a5e6..2f585b1c6e6f 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recording", ""), ("Directory", ""), ("Automatically record incoming sessions", ""), + ("Automatically record outgoing sessions", ""), ("Change", ""), ("Start session recording", ""), ("Stop session recording", ""), diff --git a/src/lang/es.rs b/src/lang/es.rs index 25cd7bf283c2..92015df0b20b 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recording", "Grabando"), ("Directory", "Directorio"), ("Automatically record incoming sessions", "Grabación automática de sesiones entrantes"), + ("Automatically record outgoing sessions", ""), ("Change", "Cambiar"), ("Start session recording", "Comenzar grabación de sesión"), ("Stop session recording", "Detener grabación de sesión"), diff --git a/src/lang/et.rs b/src/lang/et.rs index 8187759d5755..96ca16f964f6 100644 --- a/src/lang/et.rs +++ b/src/lang/et.rs @@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recording", ""), ("Directory", ""), ("Automatically record incoming sessions", ""), + ("Automatically record outgoing sessions", ""), ("Change", ""), ("Start session recording", ""), ("Stop session recording", ""), diff --git a/src/lang/eu.rs b/src/lang/eu.rs index f8ba679a756d..d68e5c42aeb2 100644 --- a/src/lang/eu.rs +++ b/src/lang/eu.rs @@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recording", "Grabatzen"), ("Directory", "Direktorioa"), ("Automatically record incoming sessions", "Automatikoki grabatu sarrerako saioak"), + ("Automatically record outgoing sessions", ""), ("Change", "Aldatu"), ("Start session recording", "Hasi saioaren grabaketa"), ("Stop session recording", "Gelditu saioaren grabaketa"), diff --git a/src/lang/fa.rs b/src/lang/fa.rs index cf5ce0f0f70e..207dfbbdbd98 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recording", "در حال ضبط"), ("Directory", "مسیر"), ("Automatically record incoming sessions", "ضبط خودکار جلسات ورودی"), + ("Automatically record outgoing sessions", ""), ("Change", "تغییر"), ("Start session recording", "شروع ضبط جلسه"), ("Stop session recording", "توقف ضبط جلسه"), diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 0fd093688b1f..9844167404ab 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recording", "Enregistrement"), ("Directory", "Répertoire"), ("Automatically record incoming sessions", "Enregistrement automatique des sessions entrantes"), + ("Automatically record outgoing sessions", ""), ("Change", "Modifier"), ("Start session recording", "Commencer l'enregistrement"), ("Stop session recording", "Stopper l'enregistrement"), diff --git a/src/lang/he.rs b/src/lang/he.rs index e36252afe903..408829b6c641 100644 --- a/src/lang/he.rs +++ b/src/lang/he.rs @@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recording", ""), ("Directory", ""), ("Automatically record incoming sessions", ""), + ("Automatically record outgoing sessions", ""), ("Change", ""), ("Start session recording", ""), ("Stop session recording", ""), diff --git a/src/lang/hr.rs b/src/lang/hr.rs index e2480eb63502..b9f9409fc394 100644 --- a/src/lang/hr.rs +++ b/src/lang/hr.rs @@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recording", "Snimanje"), ("Directory", "Mapa"), ("Automatically record incoming sessions", "Automatski snimi dolazne sesije"), + ("Automatically record outgoing sessions", ""), ("Change", "Promijeni"), ("Start session recording", "Započni snimanje sesije"), ("Stop session recording", "Zaustavi snimanje sesije"), diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 00c56edfbcad..e9caf1917f45 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recording", "Felvétel"), ("Directory", "Könyvtár"), ("Automatically record incoming sessions", "A bejövő munkamenetek automatikus rögzítése"), + ("Automatically record outgoing sessions", ""), ("Change", "Változtatás"), ("Start session recording", "Munkamenet rögzítés indítása"), ("Stop session recording", "Munkamenet rögzítés leállítása"), diff --git a/src/lang/id.rs b/src/lang/id.rs index 2f95411f0823..52c1741915e0 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recording", "Perekaman"), ("Directory", "Direktori"), ("Automatically record incoming sessions", "Otomatis merekam sesi masuk"), + ("Automatically record outgoing sessions", ""), ("Change", "Ubah"), ("Start session recording", "Mulai sesi perekaman"), ("Stop session recording", "Hentikan sesi perekaman"), diff --git a/src/lang/it.rs b/src/lang/it.rs index baaa1d10d31e..4f85db09a150 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recording", "Registrazione"), ("Directory", "Cartella"), ("Automatically record incoming sessions", "Registra automaticamente le sessioni in entrata"), + ("Automatically record outgoing sessions", ""), ("Change", "Modifica"), ("Start session recording", "Inizia registrazione sessione"), ("Stop session recording", "Ferma registrazione sessione"), diff --git a/src/lang/ja.rs b/src/lang/ja.rs index dbc40b789796..0bca730dc7c2 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recording", "録画"), ("Directory", "ディレクトリ"), ("Automatically record incoming sessions", "受信したセッションを自動で記録する"), + ("Automatically record outgoing sessions", ""), ("Change", "変更"), ("Start session recording", "セッションの録画を開始"), ("Stop session recording", "セッションの録画を停止"), diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 966ac90097e8..dc9a0a69d595 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recording", "녹화"), ("Directory", "경로"), ("Automatically record incoming sessions", "들어오는 세션을 자동으로 녹화"), + ("Automatically record outgoing sessions", ""), ("Change", "변경"), ("Start session recording", "세션 녹화 시작"), ("Stop session recording", "세션 녹화 중지"), diff --git a/src/lang/kz.rs b/src/lang/kz.rs index d62bbc393bf8..9ea54b975407 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recording", ""), ("Directory", ""), ("Automatically record incoming sessions", ""), + ("Automatically record outgoing sessions", ""), ("Change", ""), ("Start session recording", ""), ("Stop session recording", ""), diff --git a/src/lang/lt.rs b/src/lang/lt.rs index bc7d856b1e8e..df795401b762 100644 --- a/src/lang/lt.rs +++ b/src/lang/lt.rs @@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recording", "Įrašymas"), ("Directory", "Katalogas"), ("Automatically record incoming sessions", "Automatiškai įrašyti įeinančius seansus"), + ("Automatically record outgoing sessions", ""), ("Change", "Keisti"), ("Start session recording", "Pradėti seanso įrašinėjimą"), ("Stop session recording", "Sustabdyti seanso įrašinėjimą"), diff --git a/src/lang/lv.rs b/src/lang/lv.rs index bf67345c3a41..9a54daa005ba 100644 --- a/src/lang/lv.rs +++ b/src/lang/lv.rs @@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recording", "Ierakstīšana"), ("Directory", "Direktorija"), ("Automatically record incoming sessions", "Automātiski ierakstīt ienākošās sesijas"), + ("Automatically record outgoing sessions", ""), ("Change", "Mainīt"), ("Start session recording", "Sākt sesijas ierakstīšanu"), ("Stop session recording", "Apturēt sesijas ierakstīšanu"), diff --git a/src/lang/nb.rs b/src/lang/nb.rs index d91eb93258d5..4c8b1550c5e8 100644 --- a/src/lang/nb.rs +++ b/src/lang/nb.rs @@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recording", "Opptak"), ("Directory", "Mappe"), ("Automatically record incoming sessions", "Ta opp innkommende sesjoner automatisk"), + ("Automatically record outgoing sessions", ""), ("Change", "Rediger"), ("Start session recording", "Start sesjonsopptak"), ("Stop session recording", "Stopp sesjonsopptak"), diff --git a/src/lang/nl.rs b/src/lang/nl.rs index b780ce7daf72..2728f2edc899 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recording", "Opnemen"), ("Directory", "Map"), ("Automatically record incoming sessions", "Automatisch inkomende sessies opnemen"), + ("Automatically record outgoing sessions", ""), ("Change", "Wissel"), ("Start session recording", "Start de sessieopname"), ("Stop session recording", "Stop de sessieopname"), diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 8e9989084491..caf992978c9d 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recording", "Nagrywanie"), ("Directory", "Folder"), ("Automatically record incoming sessions", "Automatycznie nagrywaj sesje przychodzące"), + ("Automatically record outgoing sessions", ""), ("Change", "Zmień"), ("Start session recording", "Zacznij nagrywać sesję"), ("Stop session recording", "Zatrzymaj nagrywanie sesji"), diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index 4c01d0b628a2..1194c11ec947 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recording", ""), ("Directory", ""), ("Automatically record incoming sessions", ""), + ("Automatically record outgoing sessions", ""), ("Change", ""), ("Start session recording", ""), ("Stop session recording", ""), diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index 240fae99ae42..012ca3538fab 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recording", "Gravando"), ("Directory", "Diretório"), ("Automatically record incoming sessions", "Gravar automaticamente sessões de entrada"), + ("Automatically record outgoing sessions", ""), ("Change", "Alterar"), ("Start session recording", "Iniciar gravação da sessão"), ("Stop session recording", "Parar gravação da sessão"), diff --git a/src/lang/ro.rs b/src/lang/ro.rs index 26858b134363..e09888c58a87 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recording", "Înregistrare"), ("Directory", "Director"), ("Automatically record incoming sessions", "Înregistrează automat sesiunile viitoare"), + ("Automatically record outgoing sessions", ""), ("Change", "Modifică"), ("Start session recording", "Începe înregistrarea"), ("Stop session recording", "Oprește înregistrarea"), diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 00ae750f27db..b547208d7ab7 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recording", "Запись"), ("Directory", "Папка"), ("Automatically record incoming sessions", "Автоматически записывать входящие сеансы"), + ("Automatically record outgoing sessions", ""), ("Change", "Изменить"), ("Start session recording", "Начать запись сеанса"), ("Stop session recording", "Остановить запись сеанса"), diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 28e322460df7..bb8e872c7cf6 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recording", "Nahrávanie"), ("Directory", "Adresár"), ("Automatically record incoming sessions", "Automaticky nahrávať prichádzajúce relácie"), + ("Automatically record outgoing sessions", ""), ("Change", "Zmeniť"), ("Start session recording", "Spustiť záznam relácie"), ("Stop session recording", "Zastaviť záznam relácie"), diff --git a/src/lang/sl.rs b/src/lang/sl.rs index e7f6248160be..1563a02c5737 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recording", "Snemanje"), ("Directory", "Imenik"), ("Automatically record incoming sessions", "Samodejno snemaj vhodne seje"), + ("Automatically record outgoing sessions", ""), ("Change", "Spremeni"), ("Start session recording", "Začni snemanje seje"), ("Stop session recording", "Ustavi snemanje seje"), diff --git a/src/lang/sq.rs b/src/lang/sq.rs index 58dc1ed5502d..ccc4b805fac7 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recording", "Regjistrimi"), ("Directory", "Direktoria"), ("Automatically record incoming sessions", "Regjistro automatikisht seancat hyrëse"), + ("Automatically record outgoing sessions", ""), ("Change", "Ndrysho"), ("Start session recording", "Fillo regjistrimin e sesionit"), ("Stop session recording", "Ndalo regjistrimin e sesionit"), diff --git a/src/lang/sr.rs b/src/lang/sr.rs index d38d20e9ef34..6df6b7ad83b4 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recording", "Snimanje"), ("Directory", "Direktorijum"), ("Automatically record incoming sessions", "Automatski snimaj dolazne sesije"), + ("Automatically record outgoing sessions", ""), ("Change", "Promeni"), ("Start session recording", "Započni snimanje sesije"), ("Stop session recording", "Zaustavi snimanje sesije"), diff --git a/src/lang/sv.rs b/src/lang/sv.rs index cfbac29b9034..2f95488bd42d 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recording", "Spelar in"), ("Directory", "Katalog"), ("Automatically record incoming sessions", "Spela in inkommande sessioner automatiskt"), + ("Automatically record outgoing sessions", ""), ("Change", "Byt"), ("Start session recording", "Starta inspelning"), ("Stop session recording", "Avsluta inspelning"), diff --git a/src/lang/template.rs b/src/lang/template.rs index 1c4606e94797..962506b99eca 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recording", ""), ("Directory", ""), ("Automatically record incoming sessions", ""), + ("Automatically record outgoing sessions", ""), ("Change", ""), ("Start session recording", ""), ("Stop session recording", ""), diff --git a/src/lang/th.rs b/src/lang/th.rs index 2dbb564e870e..673ebf319812 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recording", "การบันทึก"), ("Directory", "ไดเรกทอรี่"), ("Automatically record incoming sessions", "บันทึกเซสชันขาเข้าโดยอัตโนมัติ"), + ("Automatically record outgoing sessions", ""), ("Change", "เปลี่ยน"), ("Start session recording", "เริ่มต้นการบันทึกเซสชัน"), ("Stop session recording", "หยุดการบันทึกเซสซัน"), diff --git a/src/lang/tr.rs b/src/lang/tr.rs index 9bdbec110c60..046872878938 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recording", "Kayıt Ediliyor"), ("Directory", "Klasör"), ("Automatically record incoming sessions", "Gelen oturumları otomatik olarak kayıt et"), + ("Automatically record outgoing sessions", ""), ("Change", "Değiştir"), ("Start session recording", "Oturum kaydını başlat"), ("Stop session recording", "Oturum kaydını sonlandır"), diff --git a/src/lang/tw.rs b/src/lang/tw.rs index b37949206ace..25da736b8374 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recording", "錄製"), ("Directory", "路徑"), ("Automatically record incoming sessions", "自動錄製連入的工作階段"), + ("Automatically record outgoing sessions", ""), ("Change", "變更"), ("Start session recording", "開始錄影"), ("Stop session recording", "停止錄影"), diff --git a/src/lang/uk.rs b/src/lang/uk.rs index 2bce8cc81735..97743266cad1 100644 --- a/src/lang/uk.rs +++ b/src/lang/uk.rs @@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recording", "Запис"), ("Directory", "Директорія"), ("Automatically record incoming sessions", "Автоматично записувати вхідні сеанси"), + ("Automatically record outgoing sessions", ""), ("Change", "Змінити"), ("Start session recording", "Розпочати запис сеансу"), ("Stop session recording", "Закінчити запис сеансу"), diff --git a/src/lang/vn.rs b/src/lang/vn.rs index 9693d35c3c8e..b49aea67bb76 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recording", "Đang ghi hình"), ("Directory", "Thư mục"), ("Automatically record incoming sessions", "Tự động ghi những phiên kết nối vào"), + ("Automatically record outgoing sessions", ""), ("Change", "Thay đổi"), ("Start session recording", "Bắt đầu ghi hình phiên kết nối"), ("Stop session recording", "Dừng ghi hình phiên kết nối"), diff --git a/src/server/video_service.rs b/src/server/video_service.rs index aeff1911e017..55bfa08f0e69 100644 --- a/src/server/video_service.rs +++ b/src/server/video_service.rs @@ -487,6 +487,8 @@ fn run(vs: VideoService) -> ResultType<()> { let repeat_encode_max = 10; let mut encode_fail_counter = 0; let mut first_frame = true; + let capture_width = c.width; + let capture_height = c.height; while sp.ok() { #[cfg(windows)] @@ -576,6 +578,8 @@ fn run(vs: VideoService) -> ResultType<()> { recorder.clone(), &mut encode_fail_counter, &mut first_frame, + capture_width, + capture_height, )?; frame_controller.set_send(now, send_conn_ids); } @@ -632,6 +636,8 @@ fn run(vs: VideoService) -> ResultType<()> { recorder.clone(), &mut encode_fail_counter, &mut first_frame, + capture_width, + capture_height, )?; frame_controller.set_send(now, send_conn_ids); } @@ -722,7 +728,13 @@ fn setup_encoder( ); Encoder::set_fallback(&encoder_cfg); let codec_format = Encoder::negotiated_codec(); - let recorder = get_recorder(c.width, c.height, &codec_format, record_incoming); + let recorder = get_recorder( + c.width, + c.height, + &codec_format, + record_incoming, + display_idx, + ); let use_i444 = Encoder::use_i444(&encoder_cfg); let encoder = Encoder::new(encoder_cfg.clone(), use_i444)?; Ok((encoder, encoder_cfg, codec_format, use_i444, recorder)) @@ -809,6 +821,7 @@ fn get_recorder( height: usize, codec_format: &CodecFormat, record_incoming: bool, + display: usize, ) -> Arc>> { #[cfg(windows)] let root = crate::platform::is_root(); @@ -828,10 +841,7 @@ fn get_recorder( server: true, id: Config::get_id(), dir: crate::ui_interface::video_save_directory(root), - filename: "".to_owned(), - width, - height, - format: codec_format.clone(), + display, tx, }) .map_or(Default::default(), |r| Arc::new(Mutex::new(Some(r)))) @@ -910,6 +920,8 @@ fn handle_one_frame( recorder: Arc>>, encode_fail_counter: &mut usize, first_frame: &mut bool, + width: usize, + height: usize, ) -> ResultType> { sp.snapshot(|sps| { // so that new sub and old sub share the same encoder after switch @@ -933,7 +945,7 @@ fn handle_one_frame( .lock() .unwrap() .as_mut() - .map(|r| r.write_message(&msg)); + .map(|r| r.write_message(&msg, width, height)); send_conn_ids = sp.send_video_frame(msg); } Err(e) => { diff --git a/src/ui/header.tis b/src/ui/header.tis index 36aa624b48f4..3116f1f542fa 100644 --- a/src/ui/header.tis +++ b/src/ui/header.tis @@ -301,26 +301,12 @@ class Header: Reactor.Component { } event click $(span#recording) (_, me) { - recording = !recording; header.update(); - handler.record_status(recording); - // 0 is just a dummy value. It will be ignored by the handler. - if (recording) { - handler.refresh_video(0); - if (handler.version_cmp(pi.version, '1.2.4') >= 0) handler.record_screen(recording, pi.current_display, display_width, display_height); - } - else { - handler.record_screen(recording, pi.current_display, display_width, display_height); - } + handler.record_screen(!recording) } event click $(#screen) (_, me) { if (pi.current_display == me.index) return; - if (recording) { - recording = false; - handler.record_screen(false, pi.current_display, display_width, display_height); - handler.record_status(false); - } handler.switch_display(me.index); } @@ -518,6 +504,7 @@ if (!(is_file_transfer || is_port_forward)) { handler.updatePi = function(v) { pi = v; + recording = handler.is_recording(); header.update(); if (is_port_forward) { view.windowState = View.WINDOW_MINIMIZED; @@ -682,3 +669,8 @@ handler.setConnectionType = function(secured, direct) { direct_connection: direct, }); } + +handler.updateRecordStatus = function(status) { + recording = status; + header.update(); +} \ No newline at end of file diff --git a/src/ui/index.tis b/src/ui/index.tis index b10df76233eb..daf2c10201e8 100644 --- a/src/ui/index.tis +++ b/src/ui/index.tis @@ -253,10 +253,12 @@ class Enhancements: Reactor.Component { var root_dir = show_root_dir ? handler.video_save_directory(true) : ""; var ts0 = handler.get_option("enable-record-session") == '' ? { checked: true } : {}; var ts1 = handler.get_option("allow-auto-record-incoming") == 'Y' ? { checked: true } : {}; + var ts2 = handler.get_option("allow-auto-record-outgoing") == 'Y' ? { checked: true } : {}; msgbox("custom-recording", translate('Recording'),
    {translate('Enable recording session')}
    {translate('Automatically record incoming sessions')}
    +
    {translate('Automatically record outgoing sessions')}
    {show_root_dir ?
    {translate("Incoming")}:  {root_dir}
    : ""}
    {translate(show_root_dir ? "Outgoing" : "Directory")}:  {user_dir}
    @@ -267,6 +269,7 @@ class Enhancements: Reactor.Component { if (!res) return; handler.set_option("enable-record-session", res.enable_record_session ? '' : 'N'); handler.set_option("allow-auto-record-incoming", res.auto_record_incoming ? 'Y' : ''); + handler.set_option("allow-auto-record-outgoing", res.auto_record_outgoing ? 'Y' : ''); handler.set_option("video-save-directory", $(#folderPath).text); }); } diff --git a/src/ui/remote.rs b/src/ui/remote.rs index baf9d1f64512..f0829e75eee4 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -335,6 +335,10 @@ impl InvokeUiSession for SciterHandler { } fn next_rgba(&self, _display: usize) {} + + fn update_record_status(&self, start: bool) { + self.call("updateRecordStatus", &make_args!(start)); + } } pub struct SciterSession(Session); @@ -478,8 +482,7 @@ impl sciter::EventHandler for SciterSession { fn save_image_quality(String); fn save_custom_image_quality(i32); fn refresh_video(i32); - fn record_screen(bool, i32, i32, i32); - fn record_status(bool); + fn record_screen(bool); fn get_toggle_option(String); fn is_privacy_mode_supported(); fn toggle_option(String); @@ -496,6 +499,7 @@ impl sciter::EventHandler for SciterSession { fn close_voice_call(); fn version_cmp(String, String); fn set_selected_windows_session_id(String); + fn is_recording(); } } diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index ce28b78d8e95..00e9459db47f 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -389,22 +389,17 @@ impl Session { self.send(Data::Message(LoginConfigHandler::refresh())); } - pub fn record_screen(&self, start: bool, display: i32, w: i32, h: i32) { - self.send(Data::RecordScreen( - start, - display as usize, - w, - h, - self.get_id(), - )); - } - - pub fn record_status(&self, status: bool) { + pub fn record_screen(&self, start: bool) { let mut misc = Misc::new(); - misc.set_client_record_status(status); + misc.set_client_record_status(start); let mut msg = Message::new(); msg.set_misc(misc); self.send(Data::Message(msg)); + self.send(Data::RecordScreen(start)); + } + + pub fn is_recording(&self) -> bool { + self.lc.read().unwrap().record } pub fn save_custom_image_quality(&self, custom_image_quality: i32) { @@ -1557,6 +1552,7 @@ pub trait InvokeUiSession: Send + Sync + Clone + 'static + Sized + Default { fn set_current_display(&self, disp_idx: i32); #[cfg(feature = "flutter")] fn is_multi_ui_session(&self) -> bool; + fn update_record_status(&self, start: bool); } impl Deref for Session { From 6088920f8dcb546572fd6f0bd68eb1385a8ceb0d Mon Sep 17 00:00:00 2001 From: Andrzej Rudnik Date: Mon, 21 Oct 2024 13:55:34 +0200 Subject: [PATCH 312/541] Update pl.rs (#9713) * Update pl.rs * Update pl.rs Updated quota chars. --- src/lang/pl.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/lang/pl.rs b/src/lang/pl.rs index caf992978c9d..7ec3572e813c 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -364,7 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recording", "Nagrywanie"), ("Directory", "Folder"), ("Automatically record incoming sessions", "Automatycznie nagrywaj sesje przychodzące"), - ("Automatically record outgoing sessions", ""), + ("Automatically record outgoing sessions", "Automatycznie nagrywaj sesje wychodzące"), ("Change", "Zmień"), ("Start session recording", "Zacznij nagrywać sesję"), ("Stop session recording", "Zatrzymaj nagrywanie sesji"), @@ -645,13 +645,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Parent directory", "Folder nadrzędny"), ("Resume", "Wznów"), ("Invalid file name", "Nieprawidłowa nazwa pliku"), - ("one-way-file-transfer-tip", ""), - ("Authentication Required", ""), - ("Authenticate", ""), - ("web_id_input_tip", ""), - ("Download", ""), - ("Upload folder", ""), - ("Upload files", ""), - ("Clipboard is synchronized", ""), + ("one-way-file-transfer-tip", "Jednokierunkowy transfer plików jest włączony po stronie kontrolowanej."), + ("Authentication Required", "Wymagana autoryzacja"), + ("Authenticate", "Uwierzytelnienie"), + ("web_id_input_tip", "Jeśli chcesz uzyskać dostęp do urządzenia na innym serwerze, dodaj adres serwera (@?key=) na przykład, \n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nJeśli chcesz uzyskać dostęp do urządzenia na serwerze publicznym, wprowadź \"@public\", klucz nie jest wymagany dla serwera publicznego."), + ("Download", "Pobierz"), + ("Upload folder", "Wyślij folder"), + ("Upload files", "Wyślij pliki"), + ("Clipboard is synchronized", "Schowek jest zsynchronizowany"), ].iter().cloned().collect(); } From 6159449eba86a9185accec5cc8aee423efdc8382 Mon Sep 17 00:00:00 2001 From: 21pages Date: Tue, 22 Oct 2024 08:24:52 +0800 Subject: [PATCH 313/541] move option `video-save-directory` and `allow-auto-record-outgoing` to local (#9715) Signed-off-by: 21pages --- .../desktop/pages/desktop_setting_page.dart | 5 +++-- flutter/lib/mobile/pages/settings_page.dart | 6 +++--- libs/hbb_common/src/config.rs | 19 +++++++++++++++++-- src/client.rs | 2 +- src/ipc.rs | 2 +- src/ui/index.tis | 6 +++--- src/ui_interface.rs | 8 ++++++-- 7 files changed, 34 insertions(+), 14 deletions(-) diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 766c160e042c..15cf2173b5e9 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -580,7 +580,8 @@ class _GeneralState extends State<_General> { kOptionAllowAutoRecordIncoming), if (!bind.isIncomingOnly()) _OptionCheckBox(context, 'Automatically record outgoing sessions', - kOptionAllowAutoRecordOutgoing), + kOptionAllowAutoRecordOutgoing, + isServer: false), if (showRootDir && !bind.isOutgoingOnly()) Row( children: [ @@ -635,7 +636,7 @@ class _GeneralState extends State<_General> { await FilePicker.platform.getDirectoryPath( initialDirectory: initialDirectory); if (selectedDirectory != null) { - await bind.mainSetOption( + await bind.mainSetLocalOption( key: kOptionVideoSaveDirectory, value: selectedDirectory); setState(() {}); diff --git a/flutter/lib/mobile/pages/settings_page.dart b/flutter/lib/mobile/pages/settings_page.dart index e70ee5b35c7a..eb3865933143 100644 --- a/flutter/lib/mobile/pages/settings_page.dart +++ b/flutter/lib/mobile/pages/settings_page.dart @@ -106,7 +106,7 @@ class _SettingsState extends State with WidgetsBindingObserver { _autoRecordIncomingSession = option2bool(kOptionAllowAutoRecordIncoming, bind.mainGetOptionSync(key: kOptionAllowAutoRecordIncoming)); _autoRecordOutgoingSession = option2bool(kOptionAllowAutoRecordOutgoing, - bind.mainGetOptionSync(key: kOptionAllowAutoRecordOutgoing)); + bind.mainGetLocalOption(key: kOptionAllowAutoRecordOutgoing)); _localIP = bind.mainGetOptionSync(key: 'local-ip-addr'); _directAccessPort = bind.mainGetOptionSync(key: kOptionDirectAccessPort); _allowAutoDisconnect = option2bool(kOptionAllowAutoDisconnect, @@ -711,13 +711,13 @@ class _SettingsState extends State with WidgetsBindingObserver { onToggle: isOptionFixed(kOptionAllowAutoRecordOutgoing) ? null : (v) async { - await bind.mainSetOption( + await bind.mainSetLocalOption( key: kOptionAllowAutoRecordOutgoing, value: bool2option( kOptionAllowAutoRecordOutgoing, v)); final newValue = option2bool( kOptionAllowAutoRecordOutgoing, - await bind.mainGetOption( + bind.mainGetLocalOption( key: kOptionAllowAutoRecordOutgoing)); setState(() { _autoRecordOutgoingSession = newValue; diff --git a/libs/hbb_common/src/config.rs b/libs/hbb_common/src/config.rs index 94f4ec9d988d..0cb370cd812e 100644 --- a/libs/hbb_common/src/config.rs +++ b/libs/hbb_common/src/config.rs @@ -1562,6 +1562,21 @@ impl LocalConfig { .unwrap_or_default() } + // Usually get_option should be used. + pub fn get_option_from_file(k: &str) -> String { + get_or( + &OVERWRITE_LOCAL_SETTINGS, + &Self::load().options, + &DEFAULT_LOCAL_SETTINGS, + k, + ) + .unwrap_or_default() + } + + pub fn get_bool_option(k: &str) -> bool { + option2bool(k, &Self::get_option(k)) + } + pub fn set_option(k: String, v: String) { if !is_option_can_save(&OVERWRITE_LOCAL_SETTINGS, &k, &DEFAULT_LOCAL_SETTINGS, &v) { return; @@ -2326,6 +2341,8 @@ pub mod keys { OPTION_DISABLE_GROUP_PANEL, OPTION_PRE_ELEVATE_SERVICE, OPTION_ALLOW_REMOTE_CM_MODIFICATION, + OPTION_ALLOW_AUTO_RECORD_OUTGOING, + OPTION_VIDEO_SAVE_DIRECTORY, ]; // DEFAULT_SETTINGS, OVERWRITE_SETTINGS pub const KEYS_SETTINGS: &[&str] = &[ @@ -2347,8 +2364,6 @@ pub mod keys { OPTION_AUTO_DISCONNECT_TIMEOUT, OPTION_ALLOW_ONLY_CONN_WINDOW_OPEN, OPTION_ALLOW_AUTO_RECORD_INCOMING, - OPTION_ALLOW_AUTO_RECORD_OUTGOING, - OPTION_VIDEO_SAVE_DIRECTORY, OPTION_ENABLE_ABR, OPTION_ALLOW_REMOVE_WALLPAPER, OPTION_ALLOW_ALWAYS_SOFTWARE_RENDER, diff --git a/src/client.rs b/src/client.rs index a1b2b83d4366..4b86d189b0c3 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1442,7 +1442,7 @@ impl LoginConfigHandler { self.adapter_luid = adapter_luid; self.selected_windows_session_id = None; self.shared_password = shared_password; - self.record = Config::get_bool_option(OPTION_ALLOW_AUTO_RECORD_OUTGOING); + self.record = LocalConfig::get_bool_option(OPTION_ALLOW_AUTO_RECORD_OUTGOING); } /// Check if the client should auto login. diff --git a/src/ipc.rs b/src/ipc.rs index c7243d821708..81693a735587 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -890,7 +890,7 @@ pub async fn set_data(data: &Data) -> ResultType<()> { set_data_async(data).await } -pub async fn set_data_async(data: &Data) -> ResultType<()> { +async fn set_data_async(data: &Data) -> ResultType<()> { let mut c = connect(1000, "").await?; c.send(data).await?; Ok(()) diff --git a/src/ui/index.tis b/src/ui/index.tis index daf2c10201e8..3ae54637f4b3 100644 --- a/src/ui/index.tis +++ b/src/ui/index.tis @@ -253,7 +253,7 @@ class Enhancements: Reactor.Component { var root_dir = show_root_dir ? handler.video_save_directory(true) : ""; var ts0 = handler.get_option("enable-record-session") == '' ? { checked: true } : {}; var ts1 = handler.get_option("allow-auto-record-incoming") == 'Y' ? { checked: true } : {}; - var ts2 = handler.get_option("allow-auto-record-outgoing") == 'Y' ? { checked: true } : {}; + var ts2 = handler.get_local_option("allow-auto-record-outgoing") == 'Y' ? { checked: true } : {}; msgbox("custom-recording", translate('Recording'),
    {translate('Enable recording session')}
    @@ -269,8 +269,8 @@ class Enhancements: Reactor.Component { if (!res) return; handler.set_option("enable-record-session", res.enable_record_session ? '' : 'N'); handler.set_option("allow-auto-record-incoming", res.auto_record_incoming ? 'Y' : ''); - handler.set_option("allow-auto-record-outgoing", res.auto_record_outgoing ? 'Y' : ''); - handler.set_option("video-save-directory", $(#folderPath).text); + handler.set_local_option("allow-auto-record-outgoing", res.auto_record_outgoing ? 'Y' : ''); + handler.set_local_option("video-save-directory", $(#folderPath).text); }); } this.toggleMenuState(); diff --git a/src/ui_interface.rs b/src/ui_interface.rs index e9f2875be5dd..bab54c79a3cc 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -212,7 +212,7 @@ pub fn get_builtin_option(key: &str) -> String { #[inline] pub fn set_local_option(key: String, value: String) { - LocalConfig::set_option(key, value); + LocalConfig::set_option(key.clone(), value.clone()); } #[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))] @@ -844,7 +844,11 @@ pub fn video_save_directory(root: bool) -> String { return dir.to_string_lossy().to_string(); } } - let dir = Config::get_option("video-save-directory"); + // Get directory from config file otherwise --server will use the old value from global var. + #[cfg(any(target_os = "linux", target_os = "macos"))] + let dir = LocalConfig::get_option_from_file(OPTION_VIDEO_SAVE_DIRECTORY); + #[cfg(not(any(target_os = "linux", target_os = "macos")))] + let dir = LocalConfig::get_option(OPTION_VIDEO_SAVE_DIRECTORY); if !dir.is_empty() { return dir; } From 2cdaca0fa35670ac604a5e5316fe185cdc471d03 Mon Sep 17 00:00:00 2001 From: bovirus <1262554+bovirus@users.noreply.github.com> Date: Tue, 22 Oct 2024 10:20:35 +0200 Subject: [PATCH 314/541] Update Italian language (#9718) --- src/lang/it.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lang/it.rs b/src/lang/it.rs index 4f85db09a150..426cb7c901c4 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -363,8 +363,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Unpin Toolbar", "Sblocca barra strumenti"), ("Recording", "Registrazione"), ("Directory", "Cartella"), - ("Automatically record incoming sessions", "Registra automaticamente le sessioni in entrata"), - ("Automatically record outgoing sessions", ""), + ("Automatically record incoming sessions", "Registra automaticamente sessioni in entrata"), + ("Automatically record outgoing sessions", "Registra automaticamente sessioni in uscita"), ("Change", "Modifica"), ("Start session recording", "Inizia registrazione sessione"), ("Stop session recording", "Ferma registrazione sessione"), From cc6f919080e541eeb1eecd32765dd5d68d3f69ae Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Wed, 23 Oct 2024 09:28:39 +0800 Subject: [PATCH 315/541] feat: mobile map mode (#9717) Signed-off-by: fufesou --- Cargo.lock | 2 +- flutter/lib/models/input_model.dart | 3 +- src/flutter_ffi.rs | 4 ++ src/keyboard.rs | 62 ++++++++++++++++++----------- src/ui_session_interface.rs | 37 ++++++++--------- 5 files changed, 64 insertions(+), 44 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 923325c4ea79..7565d34eed64 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5205,7 +5205,7 @@ dependencies = [ [[package]] name = "rdev" version = "0.5.0-2" -source = "git+https://github.com/rustdesk-org/rdev#d4c1759926d693ba269e2cb8cf9f87b13e424e4e" +source = "git+https://github.com/rustdesk-org/rdev#961d25cc00c6b3ef80f444e6a7bed9872e2c35ea" dependencies = [ "cocoa 0.24.1", "core-foundation 0.9.4", diff --git a/flutter/lib/models/input_model.dart b/flutter/lib/models/input_model.dart index b57048a08e4c..c7e1e6131c3d 100644 --- a/flutter/lib/models/input_model.dart +++ b/flutter/lib/models/input_model.dart @@ -544,8 +544,7 @@ class InputModel { handleKeyDownEventModifiers(e); } - // * Currently mobile does not enable map mode - if ((isDesktop || isWebDesktop) && keyboardMode == kKeyMapMode) { + if (isMobile || (isDesktop || isWebDesktop) && keyboardMode == kKeyMapMode) { // FIXME: e.character is wrong for dead keys, eg: ^ in de newKeyboardMode( e.character ?? '', diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 979e25e8d267..5dfcebacc8fc 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -222,6 +222,10 @@ pub fn session_get_enable_trusted_devices(session_id: SessionID) -> SyncReturn) { let keyboard_mode = get_keyboard_mode_enum(keyboard_mode); - if is_long_press(&event) { return; } - - for key_event in event_to_key_events(&event, keyboard_mode, lock_modes) { + let peer = get_peer_platform().to_lowercase(); + for key_event in event_to_key_events(peer, &event, keyboard_mode, lock_modes) { send_key_event(&key_event); } } + pub fn process_event_with_session( + keyboard_mode: &str, + event: &Event, + lock_modes: Option, + session: &Session, + ) { + let keyboard_mode = get_keyboard_mode_enum(keyboard_mode); + if is_long_press(&event) { + return; + } + let peer = session.peer_platform().to_lowercase(); + for key_event in event_to_key_events(peer, &event, keyboard_mode, lock_modes) { + session.send_key_event(&key_event); + } + } + pub fn get_modifiers_state( alt: bool, ctrl: bool, @@ -171,6 +187,7 @@ pub mod client { } } + #[cfg(target_os = "android")] pub fn map_key_to_control_key(key: &rdev::Key) -> Option { match key { Key::Alt => Some(ControlKey::Alt), @@ -358,7 +375,6 @@ pub fn is_long_press(event: &Event) -> bool { return false; } -#[cfg(not(any(target_os = "android", target_os = "ios")))] pub fn release_remote_keys(keyboard_mode: &str) { // todo!: client quit suddenly, how to release keys? let to_release = TO_RELEASE.lock().unwrap().clone(); @@ -387,7 +403,6 @@ pub fn get_keyboard_mode_enum(keyboard_mode: &str) -> KeyboardMode { } #[inline] -#[cfg(not(any(target_os = "ios")))] pub fn is_modifier(key: &rdev::Key) -> bool { matches!( key, @@ -403,7 +418,6 @@ pub fn is_modifier(key: &rdev::Key) -> bool { } #[inline] -#[cfg(not(any(target_os = "android", target_os = "ios")))] pub fn is_numpad_rdev_key(key: &rdev::Key) -> bool { matches!( key, @@ -426,7 +440,6 @@ pub fn is_numpad_rdev_key(key: &rdev::Key) -> bool { } #[inline] -#[cfg(not(any(target_os = "android", target_os = "ios")))] pub fn is_letter_rdev_key(key: &rdev::Key) -> bool { matches!( key, @@ -462,7 +475,6 @@ pub fn is_letter_rdev_key(key: &rdev::Key) -> bool { // https://github.com/rustdesk/rustdesk/issues/8599 // We just add these keys as letter keys. #[inline] -#[cfg(not(any(target_os = "android", target_os = "ios")))] pub fn is_letter_rdev_key_ex(key: &rdev::Key) -> bool { matches!( key, @@ -471,7 +483,6 @@ pub fn is_letter_rdev_key_ex(key: &rdev::Key) -> bool { } #[inline] -#[cfg(not(any(target_os = "android", target_os = "ios")))] fn is_numpad_key(event: &Event) -> bool { matches!(event.event_type, EventType::KeyPress(key) | EventType::KeyRelease(key) if is_numpad_rdev_key(&key)) } @@ -479,12 +490,10 @@ fn is_numpad_key(event: &Event) -> bool { // Check is letter key for lock modes. // Only letter keys need to check and send Lock key state. #[inline] -#[cfg(not(any(target_os = "android", target_os = "ios")))] fn is_letter_key_4_lock_modes(event: &Event) -> bool { matches!(event.event_type, EventType::KeyPress(key) | EventType::KeyRelease(key) if (is_letter_rdev_key(&key) || is_letter_rdev_key_ex(&key))) } -#[cfg(not(any(target_os = "android", target_os = "ios")))] fn parse_add_lock_modes_modifiers( key_event: &mut KeyEvent, lock_modes: i32, @@ -555,10 +564,13 @@ fn update_modifiers_state(event: &Event) { } pub fn event_to_key_events( + mut peer: String, event: &Event, keyboard_mode: KeyboardMode, _lock_modes: Option, ) -> Vec { + peer.retain(|c| !c.is_whitespace()); + let mut key_event = KeyEvent::new(); update_modifiers_state(event); @@ -572,16 +584,9 @@ pub fn event_to_key_events( _ => {} } - let mut peer = get_peer_platform().to_lowercase(); - peer.retain(|c| !c.is_whitespace()); - key_event.mode = keyboard_mode.into(); - #[cfg(not(any(target_os = "android", target_os = "ios")))] - let mut key_events; - #[cfg(any(target_os = "android", target_os = "ios"))] - let key_events; - key_events = match keyboard_mode { + let mut key_events = match keyboard_mode { KeyboardMode::Map => map_keyboard_mode(peer.as_str(), event, key_event), KeyboardMode::Translate => translate_keyboard_mode(peer.as_str(), event, key_event), _ => { @@ -596,15 +601,14 @@ pub fn event_to_key_events( } }; - #[cfg(not(any(target_os = "android", target_os = "ios")))] let is_numpad_key = is_numpad_key(&event); - #[cfg(not(any(target_os = "android", target_os = "ios")))] if keyboard_mode != KeyboardMode::Translate || is_numpad_key { let is_letter_key = is_letter_key_4_lock_modes(&event); for key_event in &mut key_events { if let Some(lock_modes) = _lock_modes { parse_add_lock_modes_modifiers(key_event, lock_modes, is_numpad_key, is_letter_key); } else { + #[cfg(not(any(target_os = "android", target_os = "ios")))] add_lock_modes_modifiers(key_event, is_numpad_key, is_letter_key); } } @@ -617,6 +621,7 @@ pub fn send_key_event(key_event: &KeyEvent) { if let Some(session) = CUR_SESSION.lock().unwrap().as_ref() { session.send_key_event(key_event); } + #[cfg(feature = "flutter")] if let Some(session) = flutter::get_cur_session() { session.send_key_event(key_event); @@ -936,8 +941,19 @@ fn _map_keyboard_mode(_peer: &str, event: &Event, mut key_event: KeyEvent) -> Op _ => event.position_code as _, }; #[cfg(any(target_os = "android", target_os = "ios"))] - let keycode = 0; - + let keycode = match _peer { + OS_LOWER_WINDOWS => rdev::usb_hid_code_to_win_scancode(event.usb_hid as _)?, + OS_LOWER_LINUX => rdev::usb_hid_code_to_linux_code(event.usb_hid as _)?, + OS_LOWER_MACOS => { + if hbb_common::config::LocalConfig::get_kb_layout_type() == "ISO" { + rdev::usb_hid_code_to_macos_iso_code(event.usb_hid as _)? + } else { + rdev::usb_hid_code_to_macos_code(event.usb_hid as _)? + } + } + OS_LOWER_ANDROID => rdev::usb_hid_code_to_android_key_code(event.usb_hid as _)?, + _ => event.usb_hid as _, + }; key_event.set_chr(keycode as _); Some(key_event) } diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 00e9459db47f..4160561be713 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -861,24 +861,13 @@ impl Session { platform_code, position_code: position_code as _, event_type, + usb_hid: 0, #[cfg(any(target_os = "windows", target_os = "macos"))] extra_data: 0, }; - keyboard::client::process_event(keyboard_mode, &event, Some(lock_modes)); + keyboard::client::process_event_with_session(keyboard_mode, &event, Some(lock_modes), self); } - #[cfg(any(target_os = "ios"))] - pub fn handle_flutter_key_event( - &self, - _keyboard_mode: &str, - _character: &str, - _usb_hid: i32, - _lock_modes: i32, - _down_or_up: bool, - ) { - } - - #[cfg(not(any(target_os = "ios")))] pub fn handle_flutter_key_event( &self, keyboard_mode: &str, @@ -900,7 +889,6 @@ impl Session { } } - #[cfg(not(any(target_os = "ios")))] fn _handle_key_flutter_simulation( &self, _keyboard_mode: &str, @@ -925,7 +913,6 @@ impl Session { self.send_key_event(&key_event); } - #[cfg(not(any(target_os = "ios")))] fn _handle_key_non_flutter_simulation( &self, keyboard_mode: &str, @@ -936,14 +923,24 @@ impl Session { ) { let key = rdev::usb_hid_key_from_code(usb_hid as _); + #[cfg(any(target_os = "android", target_os = "ios"))] + let position_code: KeyCode = 0; + #[cfg(any(target_os = "android", target_os = "ios"))] + let platform_code: KeyCode = 0; + #[cfg(target_os = "windows")] let platform_code: u32 = rdev::win_code_from_key(key).unwrap_or(0); #[cfg(target_os = "windows")] let position_code: KeyCode = rdev::win_scancode_from_key(key).unwrap_or(0) as _; - #[cfg(not(target_os = "windows"))] + #[cfg(not(any(target_os = "windows", target_os = "android", target_os = "ios")))] let position_code: KeyCode = rdev::code_from_key(key).unwrap_or(0) as _; - #[cfg(not(any(target_os = "windows", target_os = "linux")))] + #[cfg(not(any( + target_os = "windows", + target_os = "android", + target_os = "ios", + target_os = "linux" + )))] let platform_code: u32 = position_code as _; // For translate mode. // We need to set the platform code (keysym) if is AltGr. @@ -972,10 +969,14 @@ impl Session { platform_code, position_code: position_code as _, event_type, + #[cfg(any(target_os = "android", target_os = "ios"))] + usb_hid: usb_hid as _, + #[cfg(not(any(target_os = "android", target_os = "ios")))] + usb_hid: 0, #[cfg(any(target_os = "windows", target_os = "macos"))] extra_data: 0, }; - keyboard::client::process_event(keyboard_mode, &event, Some(lock_modes)); + keyboard::client::process_event_with_session(keyboard_mode, &event, Some(lock_modes), self); } // flutter only TODO new input From dfa9519d5860fa4fcea6a7ea2bafe9049cc72f70 Mon Sep 17 00:00:00 2001 From: Alex Rijckaert Date: Wed, 23 Oct 2024 12:13:05 +0200 Subject: [PATCH 316/541] Update nl.rs (#9726) --- src/lang/nl.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/lang/nl.rs b/src/lang/nl.rs index 2728f2edc899..76f23f042499 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -649,9 +649,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Authentication Required", "Verificatie vereist"), ("Authenticate", "Verificatie"), ("web_id_input_tip", "Je kunt een ID invoeren op dezelfde server, directe IP-toegang wordt niet ondersteund in de webclient.\nAls je toegang wilt tot een apparaat op een andere server, voeg je het serveradres toe (@?key=), bijvoorbeeld,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nAls je toegang wilt krijgen tot een apparaat op een publieke server, voer dan \"@public\" in, sleutel is niet nodig voor de publieke server."), - ("Download", ""), - ("Upload folder", ""), - ("Upload files", ""), - ("Clipboard is synchronized", ""), + ("Download", "Downloaden"), + ("Upload folder", "Map uploaden"), + ("Upload files", "Bestanden uploaden"), + ("Clipboard is synchronized", "Klembord is gesynchroniseerd"), ].iter().cloned().collect(); } From 7a3e1fe64861b60b545adf7ba7bffedf7d2a0787 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Thu, 24 Oct 2024 14:33:37 +0800 Subject: [PATCH 317/541] fix: ->macos, mouse events, key flags (#9733) * fix: win->macos, mouse events, key flags Signed-off-by: fufesou * comments Signed-off-by: fufesou --------- Signed-off-by: fufesou --- libs/enigo/src/macos/macos_impl.rs | 11 +++++++++- src/server/input_service.rs | 35 ++++++++++++++++++++++++++++-- 2 files changed, 43 insertions(+), 3 deletions(-) diff --git a/libs/enigo/src/macos/macos_impl.rs b/libs/enigo/src/macos/macos_impl.rs index a1f5d2e4a9fb..e7d7d9e8d338 100644 --- a/libs/enigo/src/macos/macos_impl.rs +++ b/libs/enigo/src/macos/macos_impl.rs @@ -111,11 +111,17 @@ pub struct Enigo { double_click_interval: u32, last_click_time: Option, multiple_click: i64, + ignore_flags: bool, flags: CGEventFlags, char_to_vkey_map: Map>, } impl Enigo { + /// Set if ignore flags when posting events. + pub fn set_ignore_flags(&mut self, ignore: bool) { + self.ignore_flags = ignore; + } + /// pub fn reset_flag(&mut self) { self.flags = CGEventFlags::CGEventFlagNull; @@ -136,7 +142,9 @@ impl Enigo { } fn post(&self, event: CGEvent) { - event.set_flags(self.flags); + if !self.ignore_flags { + event.set_flags(self.flags); + } event.set_integer_value_field(EventField::EVENT_SOURCE_USER_DATA, ENIGO_INPUT_EXTRA_VALUE); event.post(CGEventTapLocation::HID); } @@ -164,6 +172,7 @@ impl Default for Enigo { double_click_interval, multiple_click: 1, last_click_time: None, + ignore_flags: false, flags: CGEventFlags::CGEventFlagNull, char_to_vkey_map: Default::default(), } diff --git a/src/server/input_service.rs b/src/server/input_service.rs index afab184abe99..3189520be0a5 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -453,6 +453,17 @@ const MOUSE_ACTIVE_DISTANCE: i32 = 5; static RECORD_CURSOR_POS_RUNNING: AtomicBool = AtomicBool::new(false); +// https://github.com/rustdesk/rustdesk/issues/9729 +// We need to do some special handling for macOS when using the legacy mode. +#[cfg(target_os = "macos")] +static LAST_KEY_LEGACY_MODE: AtomicBool = AtomicBool::new(false); +// We use enigo to simulate mouse events. Only the legacy mode uses the key flags. +#[inline] +#[cfg(target_os = "macos")] +fn enigo_ignore_flags() -> bool { + !LAST_KEY_LEGACY_MODE.load(Ordering::SeqCst) +} + pub fn try_start_record_cursor_pos() -> Option> { if RECORD_CURSOR_POS_RUNNING.load(Ordering::SeqCst) { return None; @@ -505,6 +516,19 @@ impl VirtualInputState { fn new() -> Option { VirtualInput::new( CGEventSourceStateID::CombinedSessionState, + // Note: `CGEventTapLocation::Session` will be affected by the mouse events. + // When we're simulating key events, then move the physical mouse, the key events will be affected. + // It looks like https://github.com/rustdesk/rustdesk/issues/9729#issuecomment-2432306822 + // 1. Press "Command" key in RustDesk + // 2. Move the physical mouse + // 3. Press "V" key in RustDesk + // Then the controlled side just prints "v" instead of pasting. + // + // Changing `CGEventTapLocation::Session` to `CGEventTapLocation::HID` fixes it. + // But we do not consider this as a bug, because it's not a common case, + // we consider only RustDesk operates the controlled side. + // + // https://developer.apple.com/documentation/coregraphics/cgeventtaplocation/ CGEventTapLocation::Session, ) .map(|virtual_input| Self { @@ -945,6 +969,8 @@ pub fn handle_mouse_(evt: &MouseEvent, conn: i32) { let buttons = evt.mask >> 3; let evt_type = evt.mask & 0x7; let mut en = ENIGO.lock().unwrap(); + #[cfg(target_os = "macos")] + en.set_ignore_flags(enigo_ignore_flags()); #[cfg(not(target_os = "macos"))] let mut to_release = Vec::new(); if evt_type == MOUSE_TYPE_DOWN { @@ -1662,8 +1688,7 @@ pub fn handle_key_(evt: &KeyEvent) { let is_numpad_key = false; #[cfg(any(target_os = "windows", target_os = "linux"))] let is_numpad_key = crate::keyboard::is_numpad_rdev_key(&key); - _lock_mode_handler = - Some(LockModesHandler::new_handler(evt, is_numpad_key)); + _lock_mode_handler = Some(LockModesHandler::new_handler(evt, is_numpad_key)); } } } @@ -1672,12 +1697,18 @@ pub fn handle_key_(evt: &KeyEvent) { match evt.mode.enum_value() { Ok(KeyboardMode::Map) => { + #[cfg(target_os = "macos")] + LAST_KEY_LEGACY_MODE.store(false, Ordering::SeqCst); map_keyboard_mode(evt); } Ok(KeyboardMode::Translate) => { + #[cfg(target_os = "macos")] + LAST_KEY_LEGACY_MODE.store(false, Ordering::SeqCst); translate_keyboard_mode(evt); } _ => { + #[cfg(target_os = "macos")] + LAST_KEY_LEGACY_MODE.store(true, Ordering::SeqCst); legacy_keyboard_mode(evt); } } From 445e9ac2853ed20113bf2d186ff010b959f992c7 Mon Sep 17 00:00:00 2001 From: 21pages Date: Thu, 24 Oct 2024 17:20:48 +0800 Subject: [PATCH 318/541] no password required for file transfer action in remote control menu (#9731) Signed-off-by: 21pages --- flutter/lib/common.dart | 5 + flutter/lib/common/widgets/toolbar.dart | 16 +- .../lib/desktop/pages/desktop_home_page.dart | 1 + .../lib/desktop/pages/file_manager_page.dart | 3 + .../desktop/pages/file_manager_tab_page.dart | 4 +- .../lib/desktop/pages/port_forward_page.dart | 3 + .../desktop/pages/port_forward_tab_page.dart | 2 + .../lib/desktop/pages/remote_tab_page.dart | 2 +- flutter/lib/models/model.dart | 4 +- flutter/lib/utils/multi_window_manager.dart | 25 ++- src/client.rs | 41 ++++- src/flutter.rs | 2 + src/flutter_ffi.rs | 10 ++ src/server/connection.rs | 148 +++++++++++++----- src/ui/remote.rs | 4 +- src/ui_session_interface.rs | 4 + 16 files changed, 222 insertions(+), 52 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 1d59d0202d81..099a04a1569e 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -2304,16 +2304,19 @@ connectMainDesktop(String id, required bool isRDP, bool? forceRelay, String? password, + String? connToken, bool? isSharedPassword}) async { if (isFileTransfer) { await rustDeskWinManager.newFileTransfer(id, password: password, isSharedPassword: isSharedPassword, + connToken: connToken, forceRelay: forceRelay); } else if (isTcpTunneling || isRDP) { await rustDeskWinManager.newPortForward(id, isRDP, password: password, isSharedPassword: isSharedPassword, + connToken: connToken, forceRelay: forceRelay); } else { await rustDeskWinManager.newRemoteDesktop(id, @@ -2333,6 +2336,7 @@ connect(BuildContext context, String id, bool isRDP = false, bool forceRelay = false, String? password, + String? connToken, bool? isSharedPassword}) async { if (id == '') return; if (!isDesktop || desktopType == DesktopType.main) { @@ -2374,6 +2378,7 @@ connect(BuildContext context, String id, 'password': password, 'isSharedPassword': isSharedPassword, 'forceRelay': forceRelay, + 'connToken': connToken, }); } } else { diff --git a/flutter/lib/common/widgets/toolbar.dart b/flutter/lib/common/widgets/toolbar.dart index 0b56d9f4c149..269d7904d1a7 100644 --- a/flutter/lib/common/widgets/toolbar.dart +++ b/flutter/lib/common/widgets/toolbar.dart @@ -147,12 +147,23 @@ List toolbarControls(BuildContext context, String id, FFI ffi) { child: Text(translate('Reset canvas')), onPressed: () => ffi.cursorModel.reset())); } + + connectWithToken( + {required bool isFileTransfer, required bool isTcpTunneling}) { + final connToken = bind.sessionGetConnToken(sessionId: ffi.sessionId); + connect(context, id, + isFileTransfer: isFileTransfer, + isTcpTunneling: isTcpTunneling, + connToken: connToken); + } + // transferFile if (isDesktop) { v.add( TTextMenu( child: Text(translate('Transfer file')), - onPressed: () => connect(context, id, isFileTransfer: true)), + onPressed: () => + connectWithToken(isFileTransfer: true, isTcpTunneling: false)), ); } // tcpTunneling @@ -160,7 +171,8 @@ List toolbarControls(BuildContext context, String id, FFI ffi) { v.add( TTextMenu( child: Text(translate('TCP tunneling')), - onPressed: () => connect(context, id, isTcpTunneling: true)), + onPressed: () => + connectWithToken(isFileTransfer: false, isTcpTunneling: true)), ); } // note diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index 90fa67dedde1..493e4ca47bb6 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -774,6 +774,7 @@ class _DesktopHomePageState extends State isRDP: call.arguments['isRDP'], password: call.arguments['password'], forceRelay: call.arguments['forceRelay'], + connToken: call.arguments['connToken'], ); } else if (call.method == kWindowEventMoveTabToNewWindow) { final args = call.arguments.split(','); diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index ba1a37fb1541..90b8d7dcbf3c 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -58,12 +58,14 @@ class FileManagerPage extends StatefulWidget { required this.password, required this.isSharedPassword, this.tabController, + this.connToken, this.forceRelay}) : super(key: key); final String id; final String? password; final bool? isSharedPassword; final bool? forceRelay; + final String? connToken; final DesktopTabController? tabController; @override @@ -90,6 +92,7 @@ class _FileManagerPageState extends State isFileTransfer: true, password: widget.password, isSharedPassword: widget.isSharedPassword, + connToken: widget.connToken, forceRelay: widget.forceRelay); WidgetsBinding.instance.addPostFrameCallback((_) { _ffi.dialogManager diff --git a/flutter/lib/desktop/pages/file_manager_tab_page.dart b/flutter/lib/desktop/pages/file_manager_tab_page.dart index ca17ac3ff066..cc77cdd95819 100644 --- a/flutter/lib/desktop/pages/file_manager_tab_page.dart +++ b/flutter/lib/desktop/pages/file_manager_tab_page.dart @@ -48,6 +48,7 @@ class _FileManagerTabPageState extends State { isSharedPassword: params['isSharedPassword'], tabController: tabController, forceRelay: params['forceRelay'], + connToken: params['connToken'], ))); } @@ -56,7 +57,7 @@ class _FileManagerTabPageState extends State { super.initState(); rustDeskWinManager.setMethodHandler((call, fromWindowId) async { - print( + debugPrint( "[FileTransfer] call ${call.method} with args ${call.arguments} from window $fromWindowId to ${windowId()}"); // for simplify, just replace connectionId if (call.method == kWindowEventNewFileTransfer) { @@ -76,6 +77,7 @@ class _FileManagerTabPageState extends State { isSharedPassword: args['isSharedPassword'], tabController: tabController, forceRelay: args['forceRelay'], + connToken: args['connToken'], ))); } else if (call.method == "onDestroy") { tabController.clear(); diff --git a/flutter/lib/desktop/pages/port_forward_page.dart b/flutter/lib/desktop/pages/port_forward_page.dart index 5541cb8b33b9..d6d243c5026a 100644 --- a/flutter/lib/desktop/pages/port_forward_page.dart +++ b/flutter/lib/desktop/pages/port_forward_page.dart @@ -33,6 +33,7 @@ class PortForwardPage extends StatefulWidget { required this.isRDP, required this.isSharedPassword, this.forceRelay, + this.connToken, }) : super(key: key); final String id; final String? password; @@ -40,6 +41,7 @@ class PortForwardPage extends StatefulWidget { final bool isRDP; final bool? forceRelay; final bool? isSharedPassword; + final String? connToken; @override State createState() => _PortForwardPageState(); @@ -62,6 +64,7 @@ class _PortForwardPageState extends State password: widget.password, isSharedPassword: widget.isSharedPassword, forceRelay: widget.forceRelay, + connToken: widget.connToken, isRdp: widget.isRDP); Get.put(_ffi, tag: 'pf_${widget.id}'); debugPrint("Port forward page init success with id ${widget.id}"); diff --git a/flutter/lib/desktop/pages/port_forward_tab_page.dart b/flutter/lib/desktop/pages/port_forward_tab_page.dart index 812f7aa99ade..f399f7cab687 100644 --- a/flutter/lib/desktop/pages/port_forward_tab_page.dart +++ b/flutter/lib/desktop/pages/port_forward_tab_page.dart @@ -48,6 +48,7 @@ class _PortForwardTabPageState extends State { tabController: tabController, isRDP: isRDP, forceRelay: params['forceRelay'], + connToken: params['connToken'], ))); } @@ -82,6 +83,7 @@ class _PortForwardTabPageState extends State { isRDP: isRDP, tabController: tabController, forceRelay: args['forceRelay'], + connToken: args['connToken'], ))); } else if (call.method == "onDestroy") { tabController.clear(); diff --git a/flutter/lib/desktop/pages/remote_tab_page.dart b/flutter/lib/desktop/pages/remote_tab_page.dart index dc0153da0f43..efd437e1ff74 100644 --- a/flutter/lib/desktop/pages/remote_tab_page.dart +++ b/flutter/lib/desktop/pages/remote_tab_page.dart @@ -395,7 +395,7 @@ class _ConnectionTabPageState extends State { RemoteCountState.find().value = tabController.length; Future _remoteMethodHandler(call, fromWindowId) async { - print( + debugPrint( "[Remote Page] call ${call.method} with args ${call.arguments} from window $fromWindowId"); dynamic returnValue; diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index f0e4cd75f9e7..8bd0530f13cc 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -375,7 +375,7 @@ class FfiModel with ChangeNotifier { } else if (name == 'plugin_option') { handleOption(evt); } else if (name == "sync_peer_hash_password_to_personal_ab") { - if (desktopType == DesktopType.main || isWeb) { + if (desktopType == DesktopType.main || isWeb || isMobile) { final id = evt['id']; final hash = evt['hash']; if (id != null && hash != null) { @@ -2462,6 +2462,7 @@ class FFI { String? switchUuid, String? password, bool? isSharedPassword, + String? connToken, bool? forceRelay, int? tabWindowId, int? display, @@ -2498,6 +2499,7 @@ class FFI { forceRelay: forceRelay ?? false, password: password ?? '', isSharedPassword: isSharedPassword ?? false, + connToken: connToken, ); } else if (display != null) { if (displays == null) { diff --git a/flutter/lib/utils/multi_window_manager.dart b/flutter/lib/utils/multi_window_manager.dart index fa35b4fe9717..70001ffdff4c 100644 --- a/flutter/lib/utils/multi_window_manager.dart +++ b/flutter/lib/utils/multi_window_manager.dart @@ -201,6 +201,7 @@ class RustDeskMultiWindowManager { String? switchUuid, bool? isRDP, bool? isSharedPassword, + String? connToken, }) async { var params = { "type": type.index, @@ -217,6 +218,9 @@ class RustDeskMultiWindowManager { if (isSharedPassword != null) { params['isSharedPassword'] = isSharedPassword; } + if (connToken != null) { + params['connToken'] = connToken; + } final msg = jsonEncode(params); // separate window for file transfer is not supported @@ -254,8 +258,13 @@ class RustDeskMultiWindowManager { ); } - Future newFileTransfer(String remoteId, - {String? password, bool? isSharedPassword, bool? forceRelay}) async { + Future newFileTransfer( + String remoteId, { + String? password, + bool? isSharedPassword, + bool? forceRelay, + String? connToken, + }) async { return await newSession( WindowType.FileTransfer, kWindowEventNewFileTransfer, @@ -264,11 +273,18 @@ class RustDeskMultiWindowManager { password: password, forceRelay: forceRelay, isSharedPassword: isSharedPassword, + connToken: connToken, ); } - Future newPortForward(String remoteId, bool isRDP, - {String? password, bool? isSharedPassword, bool? forceRelay}) async { + Future newPortForward( + String remoteId, + bool isRDP, { + String? password, + bool? isSharedPassword, + bool? forceRelay, + String? connToken, + }) async { return await newSession( WindowType.PortForward, kWindowEventNewPortForward, @@ -278,6 +294,7 @@ class RustDeskMultiWindowManager { forceRelay: forceRelay, isRDP: isRDP, isSharedPassword: isSharedPassword, + connToken: connToken, ); } diff --git a/src/client.rs b/src/client.rs index 4b86d189b0c3..0b5293d22ced 100644 --- a/src/client.rs +++ b/src/client.rs @@ -11,6 +11,7 @@ use crossbeam_queue::ArrayQueue; use magnum_opus::{Channels::*, Decoder as AudioDecoder}; #[cfg(not(any(target_os = "android", target_os = "linux")))] use ringbuf::{ring_buffer::RbBase, Rb}; +use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; use std::{ collections::HashMap, @@ -1274,7 +1275,7 @@ impl VideoHandler { } // The source of sent password -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] enum PasswordSource { PersonalAb(Vec), SharedAb(String), @@ -1320,6 +1321,13 @@ impl PasswordSource { } } +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +struct ConnToken { + password: Vec, + password_source: PasswordSource, + session_id: u64, +} + /// Login config handler for [`Client`]. #[derive(Default)] pub struct LoginConfigHandler { @@ -1376,6 +1384,7 @@ impl LoginConfigHandler { mut force_relay: bool, adapter_luid: Option, shared_password: Option, + conn_token: Option, ) { let mut id = id; if id.contains("@") { @@ -1419,10 +1428,22 @@ impl LoginConfigHandler { let config = self.load_config(); self.remember = !config.password.is_empty(); self.config = config; - let mut sid = rand::random(); + + let conn_token = conn_token + .map(|x| serde_json::from_str::(&x).ok()) + .flatten(); + let mut sid = 0; + if let Some(token) = conn_token { + sid = token.session_id; + self.password = token.password; // use as last password + self.password_source = token.password_source; + } if sid == 0 { - // you won the lottery - sid = 1; + sid = rand::random(); + if sid == 0 { + // you won the lottery + sid = 1; + } } self.session_id = sid; self.supported_encoding = Default::default(); @@ -2223,6 +2244,18 @@ impl LoginConfigHandler { msg_out.set_misc(misc); msg_out } + + pub fn get_conn_token(&self) -> Option { + if self.password.is_empty() { + return None; + } + serde_json::to_string(&ConnToken { + password: self.password.clone(), + password_source: self.password_source.clone(), + session_id: self.session_id, + }) + .ok() + } } /// Media data. diff --git a/src/flutter.rs b/src/flutter.rs index 69266f51c1ce..a1c9c7e3441c 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -1126,6 +1126,7 @@ pub fn session_add( force_relay: bool, password: String, is_shared_password: bool, + conn_token: Option, ) -> ResultType { let conn_type = if is_file_transfer { ConnType::FILE_TRANSFER @@ -1180,6 +1181,7 @@ pub fn session_add( force_relay, get_adapter_luid(), shared_password, + conn_token, ); let session = Arc::new(session.clone()); diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 5dfcebacc8fc..7a0c5e87449d 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -121,6 +121,7 @@ pub fn session_add_sync( force_relay: bool, password: String, is_shared_password: bool, + conn_token: Option, ) -> SyncReturn { if let Err(e) = session_add( &session_id, @@ -132,6 +133,7 @@ pub fn session_add_sync( force_relay, password, is_shared_password, + conn_token, ) { SyncReturn(format!("Failed to add session with id {}, {}", &id, e)) } else { @@ -1341,6 +1343,14 @@ pub fn session_close_voice_call(session_id: SessionID) { } } +pub fn session_get_conn_token(session_id: SessionID) -> SyncReturn> { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + SyncReturn(session.get_conn_token()) + } else { + SyncReturn(None) + } +} + pub fn cm_handle_incoming_voice_call(id: i32, accept: bool) { crate::ui_cm_interface::handle_incoming_voice_call(id, accept); } diff --git a/src/server/connection.rs b/src/server/connection.rs index dbe8b9614317..dc184ceac8e0 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -64,9 +64,9 @@ pub type Sender = mpsc::UnboundedSender<(Instant, Arc)>; lazy_static::lazy_static! { static ref LOGIN_FAILURES: [Arc::>>; 2] = Default::default(); - static ref SESSIONS: Arc::>> = Default::default(); + static ref SESSIONS: Arc::>> = Default::default(); static ref ALIVE_CONNS: Arc::>> = Default::default(); - pub static ref AUTHED_CONNS: Arc::>> = Default::default(); + pub static ref AUTHED_CONNS: Arc::>> = Default::default(); static ref SWITCH_SIDES_UUID: Arc::>> = Default::default(); static ref WAKELOCK_SENDER: Arc::>> = Arc::new(Mutex::new(start_wakelock_thread())); } @@ -140,13 +140,20 @@ enum MessageInput { BlockOffPlugin(String), } -#[derive(Clone, Debug)] -struct Session { +#[derive(Clone, Debug, Hash, Eq, PartialEq)] +pub struct SessionKey { + peer_id: String, name: String, session_id: u64, +} + +#[derive(Clone, Debug)] +struct Session { last_recv_time: Arc>, random_password: String, tfa: bool, + conn_type: AuthConnType, + conn_id: i32, } #[cfg(not(any(target_os = "android", target_os = "ios")))] @@ -1131,6 +1138,7 @@ impl Connection { self.authed_conn_id = Some(self::raii::AuthedConnID::new( self.inner.id(), auth_conn_type, + self.session_key(), )); self.post_conn_audit( json!({"peer": ((&self.lr.my_id, &self.lr.my_name)), "type": conn_type}), @@ -1541,14 +1549,14 @@ impl Connection { if password::temporary_enabled() { let password = password::temporary_password(); if self.validate_one_password(password.clone()) { - SESSIONS.lock().unwrap().insert( - self.lr.my_id.clone(), + raii::AuthedConnID::insert_session( + self.session_key(), Session { - name: self.lr.my_name.clone(), - session_id: self.lr.session_id, last_recv_time: self.last_recv_time.clone(), random_password: password, tfa: false, + conn_type: self.conn_type(), + conn_id: self.inner.id(), }, ); return true; @@ -1570,21 +1578,19 @@ impl Connection { let session = SESSIONS .lock() .unwrap() - .get(&self.lr.my_id) + .get(&self.session_key()) .map(|s| s.to_owned()); // last_recv_time is a mutex variable shared with connection, can be updated lively. if let Some(mut session) = session { - if session.name == self.lr.my_name - && session.session_id == self.lr.session_id - && !self.lr.password.is_empty() + if !self.lr.password.is_empty() && (tfa && session.tfa || !tfa && self.validate_one_password(session.random_password.clone())) { session.last_recv_time = self.last_recv_time.clone(); - SESSIONS - .lock() - .unwrap() - .insert(self.lr.my_id.clone(), session); + session.conn_id = self.inner.id(); + session.conn_type = self.conn_type(); + raii::AuthedConnID::insert_session(self.session_key(), session); + log::info!("is recent session"); return true; } } @@ -1844,23 +1850,22 @@ impl Connection { let session = SESSIONS .lock() .unwrap() - .get(&self.lr.my_id) + .get(&self.session_key()) .map(|s| s.to_owned()); if let Some(mut session) = session { session.tfa = true; - SESSIONS - .lock() - .unwrap() - .insert(self.lr.my_id.clone(), session); + session.conn_id = self.inner.id(); + session.conn_type = self.conn_type(); + raii::AuthedConnID::insert_session(self.session_key(), session); } else { - SESSIONS.lock().unwrap().insert( - self.lr.my_id.clone(), + raii::AuthedConnID::insert_session( + self.session_key(), Session { - name: self.lr.my_name.clone(), - session_id: self.lr.session_id, last_recv_time: self.last_recv_time.clone(), random_password: "".to_owned(), tfa: true, + conn_type: self.conn_type(), + conn_id: self.inner.id(), }, ); } @@ -2159,12 +2164,8 @@ impl Connection { _ => {} } if let Some(job_id) = job_id { - self.send(fs::new_error( - job_id, - "one-way-file-transfer-tip", - 0, - )) - .await; + self.send(fs::new_error(job_id, "one-way-file-transfer-tip", 0)) + .await; return true; } } @@ -2399,7 +2400,10 @@ impl Connection { } Some(misc::Union::CloseReason(_)) => { self.on_close("Peer close", true).await; - SESSIONS.lock().unwrap().remove(&self.lr.my_id); + raii::AuthedConnID::remove_session_if_last_duplication( + self.inner.id(), + self.session_key(), + ); return false; } @@ -3159,7 +3163,7 @@ impl Connection { let mut msg_out = Message::new(); msg_out.set_misc(misc); self.send(msg_out).await; - SESSIONS.lock().unwrap().remove(&self.lr.my_id); + raii::AuthedConnID::remove_session_if_last_duplication(self.inner.id(), self.session_key()); } fn read_dir(&mut self, dir: &str, include_hidden: bool) { @@ -3313,6 +3317,26 @@ impl Connection { } } } + + #[inline] + fn conn_type(&self) -> AuthConnType { + if self.file_transfer.is_some() { + AuthConnType::FileTransfer + } else if self.port_forward_socket.is_some() { + AuthConnType::PortForward + } else { + AuthConnType::Remote + } + } + + #[inline] + fn session_key(&self) -> SessionKey { + SessionKey { + peer_id: self.lr.my_id.clone(), + name: self.lr.my_name.clone(), + session_id: self.lr.session_id, + } + } } pub fn insert_switch_sides_uuid(id: String, uuid: uuid::Uuid) { @@ -3810,15 +3834,18 @@ mod raii { pub struct AuthedConnID(i32, AuthConnType); impl AuthedConnID { - pub fn new(id: i32, conn_type: AuthConnType) -> Self { - AUTHED_CONNS.lock().unwrap().push((id, conn_type)); + pub fn new(conn_id: i32, conn_type: AuthConnType, session_key: SessionKey) -> Self { + AUTHED_CONNS + .lock() + .unwrap() + .push((conn_id, conn_type, session_key)); Self::check_wake_lock(); use std::sync::Once; static _ONCE: Once = Once::new(); _ONCE.call_once(|| { shutdown_hooks::add_shutdown_hook(connection_shutdown_hook); }); - Self(id, conn_type) + Self(conn_id, conn_type) } fn check_wake_lock() { @@ -3843,6 +3870,53 @@ mod raii { .filter(|c| c.1 == AuthConnType::Remote || c.1 == AuthConnType::FileTransfer) .count() } + + pub fn remove_session_if_last_duplication(conn_id: i32, key: SessionKey) { + let contains = SESSIONS.lock().unwrap().contains_key(&key); + if contains { + let another = AUTHED_CONNS + .lock() + .unwrap() + .iter() + .any(|c| c.0 != conn_id && c.2 == key && c.1 != AuthConnType::PortForward); + if !another { + // Keep the session if there is another connection with same peer_id and session_id. + SESSIONS.lock().unwrap().remove(&key); + log::info!("remove session"); + } else { + log::info!("skip remove session"); + } + } + } + + pub fn insert_session(key: SessionKey, session: Session) { + let mut insert = true; + if session.conn_type == AuthConnType::PortForward { + // port forward doesn't update last received time + let other_alive_conns = AUTHED_CONNS + .lock() + .unwrap() + .iter() + .filter(|c| { + c.2 == key && c.1 != AuthConnType::PortForward // port forward doesn't remove itself + }) + .map(|c| c.0) + .collect::>(); + let another = SESSIONS.lock().unwrap().get(&key).map(|s| { + other_alive_conns.contains(&s.conn_id) + && s.tfa == session.tfa + && s.conn_type != AuthConnType::PortForward + }) == Some(true); + if another { + insert = false; + log::info!("skip insert session for port forward"); + } + } + if insert { + log::info!("insert session for {:?}", session.conn_type); + SESSIONS.lock().unwrap().insert(key, session); + } + } } impl Drop for AuthedConnID { @@ -3850,7 +3924,7 @@ mod raii { if self.1 == AuthConnType::Remote { scrap::codec::Encoder::update(scrap::codec::EncodingUpdate::Remove(self.0)); } - AUTHED_CONNS.lock().unwrap().retain(|&c| c.0 != self.0); + AUTHED_CONNS.lock().unwrap().retain(|c| c.0 != self.0); let remote_count = AUTHED_CONNS .lock() .unwrap() diff --git a/src/ui/remote.rs b/src/ui/remote.rs index f0829e75eee4..0296d82bda56 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -506,7 +506,7 @@ impl sciter::EventHandler for SciterSession { impl SciterSession { pub fn new(cmd: String, id: String, password: String, args: Vec) -> Self { let force_relay = args.contains(&"--relay".to_string()); - let mut session: Session = Session { + let session: Session = Session { password: password.clone(), args, server_keyboard_enabled: Arc::new(RwLock::new(true)), @@ -529,7 +529,7 @@ impl SciterSession { .lc .write() .unwrap() - .initialize(id, conn_type, None, force_relay, None, None); + .initialize(id, conn_type, None, force_relay, None, None, None); Self(session) } diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 4160561be713..321707d3f63e 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -1490,6 +1490,10 @@ impl Session { msg.set_misc(misc); self.send(Data::Message(msg)); } + + pub fn get_conn_token(&self) -> Option { + self.lc.read().unwrap().get_conn_token() + } } pub trait InvokeUiSession: Send + Sync + Clone + 'static + Sized + Default { From b35b48086a3b5cf535beb726e4d9142e6461a357 Mon Sep 17 00:00:00 2001 From: Tobias Degen <40177712+tobiasdegen@users.noreply.github.com> Date: Thu, 24 Oct 2024 15:52:39 +0200 Subject: [PATCH 319/541] Add translation string for better translationj capability (#9736) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add new translation für sending Ctrl+Alt+Del * Add new translation string for sending Ctrl+Alt+Del --- flutter/lib/common/widgets/toolbar.dart | 2 +- src/lang/ar.rs | 1 + src/lang/be.rs | 1 + src/lang/bg.rs | 1 + src/lang/ca.rs | 1 + src/lang/cn.rs | 1 + src/lang/cs.rs | 1 + src/lang/da.rs | 1 + src/lang/de.rs | 1 + src/lang/el.rs | 1 + src/lang/en.rs | 1 + src/lang/eo.rs | 1 + src/lang/es.rs | 1 + src/lang/et.rs | 1 + src/lang/eu.rs | 1 + src/lang/fa.rs | 1 + src/lang/fr.rs | 1 + src/lang/he.rs | 1 + src/lang/hr.rs | 1 + src/lang/hu.rs | 1 + src/lang/id.rs | 1 + src/lang/it.rs | 1 + src/lang/ja.rs | 1 + src/lang/ko.rs | 1 + src/lang/kz.rs | 1 + src/lang/lt.rs | 1 + src/lang/lv.rs | 1 + src/lang/nb.rs | 1 + src/lang/nl.rs | 1 + src/lang/pl.rs | 1 + src/lang/pt_PT.rs | 1 + src/lang/ptbr.rs | 1 + src/lang/ro.rs | 1 + src/lang/ru.rs | 1 + src/lang/sk.rs | 1 + src/lang/sl.rs | 1 + src/lang/sq.rs | 1 + src/lang/sr.rs | 1 + src/lang/sv.rs | 1 + src/lang/template.rs | 1 + src/lang/th.rs | 1 + src/lang/tr.rs | 1 + src/lang/tw.rs | 1 + src/lang/uk.rs | 1 + src/lang/vn.rs | 1 + 45 files changed, 45 insertions(+), 1 deletion(-) diff --git a/flutter/lib/common/widgets/toolbar.dart b/flutter/lib/common/widgets/toolbar.dart index 269d7904d1a7..153121057e5e 100644 --- a/flutter/lib/common/widgets/toolbar.dart +++ b/flutter/lib/common/widgets/toolbar.dart @@ -195,7 +195,7 @@ List toolbarControls(BuildContext context, String id, FFI ffi) { (pi.platform == kPeerPlatformLinux || pi.sasEnabled)) { v.add( TTextMenu( - child: Text('${translate("Insert")} Ctrl + Alt + Del'), + child: Text('${translate("Insert Ctrl + Alt + Del")}'), onPressed: () => bind.sessionCtrlAltDel(sessionId: sessionId)), ); } diff --git a/src/lang/ar.rs b/src/lang/ar.rs index fe7e853815b6..1d413019594e 100644 --- a/src/lang/ar.rs +++ b/src/lang/ar.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), + ("Insert Ctrl + Alt + Del", ""), ].iter().cloned().collect(); } diff --git a/src/lang/be.rs b/src/lang/be.rs index da9be46401c3..5e4313357fa7 100644 --- a/src/lang/be.rs +++ b/src/lang/be.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), + ("Insert Ctrl + Alt + Del", ""), ].iter().cloned().collect(); } diff --git a/src/lang/bg.rs b/src/lang/bg.rs index 72b5fcf19717..11bd4037892b 100644 --- a/src/lang/bg.rs +++ b/src/lang/bg.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), + ("Insert Ctrl + Alt + Del", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ca.rs b/src/lang/ca.rs index fbe3cde5fcd5..7fa884ea6728 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), + ("Insert Ctrl + Alt + Del", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index f57d100d5e58..bba63cd12d9c 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", "上传文件夹"), ("Upload files", "上传文件"), ("Clipboard is synchronized", "剪贴板已同步"), + ("Insert Ctrl + Alt + Del", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index d72b85a0cf50..5dfc808b878c 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), + ("Insert Ctrl + Alt + Del", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index 558b2fa45410..004afc1f80b3 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), + ("Insert Ctrl + Alt + Del", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index 0a78ee2b55e8..33371a197f54 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", "Ordner hochladen"), ("Upload files", "Dateien hochladen"), ("Clipboard is synchronized", "Zwischenablage ist synchronisiert"), + ("Insert Ctrl + Alt + Del", "Strg + Alt + Entf senden"), ].iter().cloned().collect(); } diff --git a/src/lang/el.rs b/src/lang/el.rs index 9725ecc78864..aac308aa815a 100644 --- a/src/lang/el.rs +++ b/src/lang/el.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), + ("Insert Ctrl + Alt + Del", ""), ].iter().cloned().collect(); } diff --git a/src/lang/en.rs b/src/lang/en.rs index 7ed83a8fe4f0..ac3829661497 100644 --- a/src/lang/en.rs +++ b/src/lang/en.rs @@ -236,5 +236,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("enable-trusted-devices-tip", "Skip 2FA verification on trusted devices"), ("one-way-file-transfer-tip", "One-way file transfer is enabled on the controlled side."), ("web_id_input_tip", "You can input an ID in the same server, direct IP access is not supported in web client.\nIf you want to access a device on another server, please append the server address (@?key=), for example,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nIf you want to access a device on a public server, please input \"@public\", the key is not needed for public server."), + ("Insert Ctrl + Alt + Del", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 2f585b1c6e6f..90fa251ff4ff 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), + ("Insert Ctrl + Alt + Del", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index 92015df0b20b..72d3e14cdb5d 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", "Subir carpeta"), ("Upload files", "Subir archivos"), ("Clipboard is synchronized", "Portapapeles sincronizado"), + ("Insert Ctrl + Alt + Del", ""), ].iter().cloned().collect(); } diff --git a/src/lang/et.rs b/src/lang/et.rs index 96ca16f964f6..631ae11fd9bf 100644 --- a/src/lang/et.rs +++ b/src/lang/et.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), + ("Insert Ctrl + Alt + Del", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eu.rs b/src/lang/eu.rs index d68e5c42aeb2..22d23dfca1c5 100644 --- a/src/lang/eu.rs +++ b/src/lang/eu.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), + ("Insert Ctrl + Alt + Del", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index 207dfbbdbd98..d36a0c925c96 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), + ("Insert Ctrl + Alt + Del", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 9844167404ab..5920eb0e3ea8 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), + ("Insert Ctrl + Alt + Del", ""), ].iter().cloned().collect(); } diff --git a/src/lang/he.rs b/src/lang/he.rs index 408829b6c641..b82a6c8f3ddb 100644 --- a/src/lang/he.rs +++ b/src/lang/he.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), + ("Insert Ctrl + Alt + Del", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hr.rs b/src/lang/hr.rs index b9f9409fc394..ce591f58bcae 100644 --- a/src/lang/hr.rs +++ b/src/lang/hr.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), + ("Insert Ctrl + Alt + Del", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index e9caf1917f45..20c509594569 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), + ("Insert Ctrl + Alt + Del", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index 52c1741915e0..25fa0e5dbe8a 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), + ("Insert Ctrl + Alt + Del", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index 426cb7c901c4..4f47b4aac18f 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", "Cartella upload"), ("Upload files", "File upload"), ("Clipboard is synchronized", "Gli appunti sono sincronizzati"), + ("Insert Ctrl + Alt + Del", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 0bca730dc7c2..5934683b4a3b 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), + ("Insert Ctrl + Alt + Del", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index dc9a0a69d595..036af2a4d536 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), + ("Insert Ctrl + Alt + Del", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index 9ea54b975407..874ecfc57308 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), + ("Insert Ctrl + Alt + Del", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lt.rs b/src/lang/lt.rs index df795401b762..7f5b7b023317 100644 --- a/src/lang/lt.rs +++ b/src/lang/lt.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), + ("Insert Ctrl + Alt + Del", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lv.rs b/src/lang/lv.rs index 9a54daa005ba..0a89e02e2cfc 100644 --- a/src/lang/lv.rs +++ b/src/lang/lv.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", "Augšupielādēt mapi"), ("Upload files", "Augšupielādēt failus"), ("Clipboard is synchronized", "Starpliktuve ir sinhronizēta"), + ("Insert Ctrl + Alt + Del", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nb.rs b/src/lang/nb.rs index 4c8b1550c5e8..b07a103c15f7 100644 --- a/src/lang/nb.rs +++ b/src/lang/nb.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), + ("Insert Ctrl + Alt + Del", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nl.rs b/src/lang/nl.rs index 76f23f042499..71384cee002e 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", "Map uploaden"), ("Upload files", "Bestanden uploaden"), ("Clipboard is synchronized", "Klembord is gesynchroniseerd"), + ("Insert Ctrl + Alt + Del", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 7ec3572e813c..f6f4db4f7ecb 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", "Wyślij folder"), ("Upload files", "Wyślij pliki"), ("Clipboard is synchronized", "Schowek jest zsynchronizowany"), + ("Insert Ctrl + Alt + Del", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index 1194c11ec947..80676744855c 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), + ("Insert Ctrl + Alt + Del", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index 012ca3538fab..8d83ea394b25 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), + ("Insert Ctrl + Alt + Del", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ro.rs b/src/lang/ro.rs index e09888c58a87..7c8ea1d1e861 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), + ("Insert Ctrl + Alt + Del", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index b547208d7ab7..bdf4ba462568 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", "Загрузить папку"), ("Upload files", "Загрузить файлы"), ("Clipboard is synchronized", "Буфер обмена синхронизирован"), + ("Insert Ctrl + Alt + Del", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index bb8e872c7cf6..01d2a7507cd9 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), + ("Insert Ctrl + Alt + Del", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sl.rs b/src/lang/sl.rs index 1563a02c5737..ba20e0148c0c 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), + ("Insert Ctrl + Alt + Del", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sq.rs b/src/lang/sq.rs index ccc4b805fac7..fe398bc59945 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), + ("Insert Ctrl + Alt + Del", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sr.rs b/src/lang/sr.rs index 6df6b7ad83b4..cca6bb361add 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), + ("Insert Ctrl + Alt + Del", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sv.rs b/src/lang/sv.rs index 2f95488bd42d..0cb2f92c8f2d 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), + ("Insert Ctrl + Alt + Del", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index 962506b99eca..b015e177dbcc 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), + ("Insert Ctrl + Alt + Del", ""), ].iter().cloned().collect(); } diff --git a/src/lang/th.rs b/src/lang/th.rs index 673ebf319812..8e0711525ef0 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), + ("Insert Ctrl + Alt + Del", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index 046872878938..2d1be69d3ad5 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), + ("Insert Ctrl + Alt + Del", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 25da736b8374..8ac7c13ff412 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", "上傳資料夾"), ("Upload files", "上傳檔案"), ("Clipboard is synchronized", ""), + ("Insert Ctrl + Alt + Del", ""), ].iter().cloned().collect(); } diff --git a/src/lang/uk.rs b/src/lang/uk.rs index 97743266cad1..63e3cc199a3f 100644 --- a/src/lang/uk.rs +++ b/src/lang/uk.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), + ("Insert Ctrl + Alt + Del", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index b49aea67bb76..1454981ac00e 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), + ("Insert Ctrl + Alt + Del", ""), ].iter().cloned().collect(); } From bd22b01370b290a2fdf34aaf03a2164318516043 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Thu, 24 Oct 2024 22:27:51 +0800 Subject: [PATCH 320/541] fix "insert ctrl+alt+del" --- flutter/lib/desktop/widgets/remote_toolbar.dart | 2 +- src/lang/ar.rs | 3 +-- src/lang/be.rs | 3 +-- src/lang/bg.rs | 3 +-- src/lang/ca.rs | 3 +-- src/lang/cn.rs | 3 +-- src/lang/cs.rs | 3 +-- src/lang/da.rs | 3 +-- src/lang/de.rs | 3 +-- src/lang/el.rs | 3 +-- src/lang/en.rs | 1 - src/lang/eo.rs | 3 +-- src/lang/es.rs | 3 +-- src/lang/et.rs | 3 +-- src/lang/eu.rs | 3 +-- src/lang/fa.rs | 3 +-- src/lang/fr.rs | 3 +-- src/lang/he.rs | 3 +-- src/lang/hr.rs | 3 +-- src/lang/hu.rs | 3 +-- src/lang/id.rs | 3 +-- src/lang/it.rs | 3 +-- src/lang/ja.rs | 3 +-- src/lang/ko.rs | 3 +-- src/lang/kz.rs | 3 +-- src/lang/lt.rs | 3 +-- src/lang/lv.rs | 3 +-- src/lang/nb.rs | 3 +-- src/lang/nl.rs | 3 +-- src/lang/pl.rs | 3 +-- src/lang/pt_PT.rs | 3 +-- src/lang/ptbr.rs | 3 +-- src/lang/ro.rs | 3 +-- src/lang/ru.rs | 3 +-- src/lang/sk.rs | 3 +-- src/lang/sl.rs | 3 +-- src/lang/sq.rs | 3 +-- src/lang/sr.rs | 3 +-- src/lang/sv.rs | 3 +-- src/lang/template.rs | 3 +-- src/lang/th.rs | 3 +-- src/lang/tr.rs | 3 +-- src/lang/tw.rs | 3 +-- src/lang/uk.rs | 3 +-- src/lang/vn.rs | 3 +-- 45 files changed, 44 insertions(+), 88 deletions(-) diff --git a/flutter/lib/desktop/widgets/remote_toolbar.dart b/flutter/lib/desktop/widgets/remote_toolbar.dart index 75791ad093cc..4857464861fc 100644 --- a/flutter/lib/desktop/widgets/remote_toolbar.dart +++ b/flutter/lib/desktop/widgets/remote_toolbar.dart @@ -305,7 +305,7 @@ class RemoteMenuEntry { }) { return MenuEntryButton( childBuilder: (TextStyle? style) => Text( - '${translate("Insert")} Ctrl + Alt + Del', + '${translate("Insert Ctrl + Alt + Del")', style: style, ), proc: () { diff --git a/src/lang/ar.rs b/src/lang/ar.rs index 1d413019594e..31fd680fd606 100644 --- a/src/lang/ar.rs +++ b/src/lang/ar.rs @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "عرض مراقب الجودة"), ("Disable clipboard", "تعطيل الحافظة"), ("Lock after session end", "القفل بعد نهاية هذه الجلسة"), - ("Insert", "ادخال"), + ("Insert Ctrl + Alt + Del", "Ctrl + Alt + Del دخال"), ("Insert Lock", "قفل الادخال"), ("Refresh", "تحديث"), ("ID does not exist", "المعرف غير موجود"), @@ -653,6 +653,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), - ("Insert Ctrl + Alt + Del", ""), ].iter().cloned().collect(); } diff --git a/src/lang/be.rs b/src/lang/be.rs index 5e4313357fa7..fbe16153543a 100644 --- a/src/lang/be.rs +++ b/src/lang/be.rs @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "Паказваць манітор якасці"), ("Disable clipboard", "Адключыць буфер абмену"), ("Lock after session end", "Заблакаваць уліковы запіс пасля сеансу"), - ("Insert", "Уставіць"), + ("Insert Ctrl + Alt + Del", "Уставіць Ctrl + Alt + Del"), ("Insert Lock", "Заблакаваць уліковы запіс"), ("Refresh", "Абнавіць"), ("ID does not exist", "ID не існуе"), @@ -653,6 +653,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), - ("Insert Ctrl + Alt + Del", ""), ].iter().cloned().collect(); } diff --git a/src/lang/bg.rs b/src/lang/bg.rs index 11bd4037892b..4f0131cc878a 100644 --- a/src/lang/bg.rs +++ b/src/lang/bg.rs @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "Показвай прозорец за качество"), ("Disable clipboard", "Забрана за достъп до клипборд"), ("Lock after session end", "Заключване след край на ползване"), - ("Insert", "Поставяне"), + ("Insert Ctrl + Alt + Del", "Поставяне Ctrl + Alt + Del"), ("Insert Lock", "Заявка за заключване"), ("Refresh", "Обновяване"), ("ID does not exist", "Несъществуващ определител (ID)"), @@ -653,6 +653,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), - ("Insert Ctrl + Alt + Del", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ca.rs b/src/lang/ca.rs index 7fa884ea6728..8e0ff1479cc7 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "Mostra la informació de flux"), ("Disable clipboard", "Inhabilita el porta-retalls"), ("Lock after session end", "Bloca en finalitzar la sessió"), - ("Insert", "Insereix"), + ("Insert Ctrl + Alt + Del", "Insereix Ctrl + Alt + Del"), ("Insert Lock", "Bloca"), ("Refresh", "Actualitza"), ("ID does not exist", "Aquesta ID no existeix"), @@ -653,6 +653,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), - ("Insert Ctrl + Alt + Del", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index bba63cd12d9c..8b1d3a5f9a44 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "显示质量监测"), ("Disable clipboard", "禁用剪贴板"), ("Lock after session end", "会话结束后锁定远程电脑"), - ("Insert", "插入"), + ("Insert Ctrl + Alt + Del", "插入 Ctrl + Alt + Del"), ("Insert Lock", "锁定远程电脑"), ("Refresh", "刷新画面"), ("ID does not exist", "ID 不存在"), @@ -653,6 +653,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", "上传文件夹"), ("Upload files", "上传文件"), ("Clipboard is synchronized", "剪贴板已同步"), - ("Insert Ctrl + Alt + Del", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index 5dfc808b878c..a9fb5b233cb0 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "Zobrazit monitor kvality"), ("Disable clipboard", "Vypnout schránku"), ("Lock after session end", "Po ukončení relace zamknout plochu"), - ("Insert", "Vložit"), + ("Insert Ctrl + Alt + Del", "Vložit Ctrl + Alt + Del"), ("Insert Lock", "Zamknout"), ("Refresh", "Načíst znovu"), ("ID does not exist", "Toto ID neexistuje"), @@ -653,6 +653,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), - ("Insert Ctrl + Alt + Del", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index 004afc1f80b3..34e5433f517f 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "Vis billedkvalitet"), ("Disable clipboard", "Deaktiver udklipsholder"), ("Lock after session end", "Lås efter afslutningen af fjernstyring"), - ("Insert", "Indsæt"), + ("Insert Ctrl + Alt + Del", "Indsæt Ctrl + Alt + Del"), ("Insert Lock", "Indsæt lås"), ("Refresh", "Genopfrisk"), ("ID does not exist", "ID findes ikke"), @@ -653,6 +653,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), - ("Insert Ctrl + Alt + Del", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index 33371a197f54..e59c3b5050ca 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "Qualitätsüberwachung anzeigen"), ("Disable clipboard", "Zwischenablage deaktivieren"), ("Lock after session end", "Nach Sitzungsende sperren"), - ("Insert", "Einfügen"), + ("Insert Ctrl + Alt + Del", "Einfügen Ctrl + Alt + Del"), ("Insert Lock", "Win+L (Sperren) senden"), ("Refresh", "Aktualisieren"), ("ID does not exist", "Diese ID existiert nicht."), @@ -653,6 +653,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", "Ordner hochladen"), ("Upload files", "Dateien hochladen"), ("Clipboard is synchronized", "Zwischenablage ist synchronisiert"), - ("Insert Ctrl + Alt + Del", "Strg + Alt + Entf senden"), ].iter().cloned().collect(); } diff --git a/src/lang/el.rs b/src/lang/el.rs index aac308aa815a..e6df3bc3d5b1 100644 --- a/src/lang/el.rs +++ b/src/lang/el.rs @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "Εμφάνιση παρακολούθησης ποιότητας σύνδεσης"), ("Disable clipboard", "Απενεργοποίηση προχείρου"), ("Lock after session end", "Κλείδωμα μετά το τέλος της συνεδρίας"), - ("Insert", "Εισαγωγή"), + ("Insert Ctrl + Alt + Del", "Εισαγωγή Ctrl + Alt + Del"), ("Insert Lock", "Κλείδωμα απομακρυσμένου σταθμού"), ("Refresh", "Ανανέωση"), ("ID does not exist", "Το αναγνωριστικό ID δεν υπάρχει"), @@ -653,6 +653,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), - ("Insert Ctrl + Alt + Del", ""), ].iter().cloned().collect(); } diff --git a/src/lang/en.rs b/src/lang/en.rs index ac3829661497..7ed83a8fe4f0 100644 --- a/src/lang/en.rs +++ b/src/lang/en.rs @@ -236,6 +236,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("enable-trusted-devices-tip", "Skip 2FA verification on trusted devices"), ("one-way-file-transfer-tip", "One-way file transfer is enabled on the controlled side."), ("web_id_input_tip", "You can input an ID in the same server, direct IP access is not supported in web client.\nIf you want to access a device on another server, please append the server address (@?key=), for example,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nIf you want to access a device on a public server, please input \"@public\", the key is not needed for public server."), - ("Insert Ctrl + Alt + Del", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 90fa251ff4ff..876c901a481b 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "Montri kvalito monitoron"), ("Disable clipboard", "Malebligi poŝon"), ("Lock after session end", "Ŝlosi foran komputilon post malkonektado"), - ("Insert", "Enmeti"), + ("Insert Ctrl + Alt + Del", "Enmeti Ctrl + Alt + Del"), ("Insert Lock", "Ŝlosi foran komputilon"), ("Refresh", "Refreŝigi ekranon"), ("ID does not exist", "La identigilo ne ekzistas"), @@ -653,6 +653,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), - ("Insert Ctrl + Alt + Del", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index 72d3e14cdb5d..ce77f620c717 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "Mostrar calidad del monitor"), ("Disable clipboard", "Deshabilitar portapapeles"), ("Lock after session end", "Bloquear después del final de la sesión"), - ("Insert", "Insertar"), + ("Insert Ctrl + Alt + Del", "Insertar Ctrl + Alt + Del"), ("Insert Lock", "Insertar bloqueo"), ("Refresh", "Actualizar"), ("ID does not exist", "La ID no existe"), @@ -653,6 +653,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", "Subir carpeta"), ("Upload files", "Subir archivos"), ("Clipboard is synchronized", "Portapapeles sincronizado"), - ("Insert Ctrl + Alt + Del", ""), ].iter().cloned().collect(); } diff --git a/src/lang/et.rs b/src/lang/et.rs index 631ae11fd9bf..21de56c9e69f 100644 --- a/src/lang/et.rs +++ b/src/lang/et.rs @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", ""), ("Disable clipboard", ""), ("Lock after session end", ""), - ("Insert", ""), + ("Insert Ctrl + Alt + Del", ""), ("Insert Lock", "Sisesta lukk"), ("Refresh", ""), ("ID does not exist", ""), @@ -653,6 +653,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), - ("Insert Ctrl + Alt + Del", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eu.rs b/src/lang/eu.rs index 22d23dfca1c5..ac958d79c67e 100644 --- a/src/lang/eu.rs +++ b/src/lang/eu.rs @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "Erakutsi kalitate monitorea"), ("Disable clipboard", "Desgaitu arbela"), ("Lock after session end", "Blokeatu sesioa amaitu ostean"), - ("Insert", "Sartu"), + ("Insert Ctrl + Alt + Del", "Sartu Ctrl + Alt + Del"), ("Insert Lock", "Sarrera-blokeoa"), ("Refresh", "Freskatu"), ("ID does not exist", "IDa ez da existitzen"), @@ -653,6 +653,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), - ("Insert Ctrl + Alt + Del", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index d36a0c925c96..ff43815739a3 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "نمایش کیفیت مانیتور"), ("Disable clipboard", " غیرفعالسازی کلیپبورد"), ("Lock after session end", "قفل کردن حساب کاربری سیستم عامل پس از پایان جلسه"), - ("Insert", "افزودن"), + ("Insert Ctrl + Alt + Del", "Ctrl + Alt + Del افزودن"), ("Insert Lock", "قفل کردن سیستم"), ("Refresh", "تازه سازی"), ("ID does not exist", "شناسه وجود ندارد"), @@ -653,6 +653,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), - ("Insert Ctrl + Alt + Del", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 5920eb0e3ea8..85b1354c3184 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "Afficher le moniteur de qualité"), ("Disable clipboard", "Désactiver le presse-papier"), ("Lock after session end", "Verrouiller l'appareil distant après la déconnexion"), - ("Insert", "Envoyer"), + ("Insert Ctrl + Alt + Del", "Envoyer Ctrl + Alt + Del"), ("Insert Lock", "Verrouiller l'appareil distant"), ("Refresh", "Rafraîchir l'écran"), ("ID does not exist", "L'ID n'existe pas"), @@ -653,6 +653,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), - ("Insert Ctrl + Alt + Del", ""), ].iter().cloned().collect(); } diff --git a/src/lang/he.rs b/src/lang/he.rs index b82a6c8f3ddb..b63d42122469 100644 --- a/src/lang/he.rs +++ b/src/lang/he.rs @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", ""), ("Disable clipboard", ""), ("Lock after session end", ""), - ("Insert", ""), + ("Insert Ctrl + Alt + Del", ""), ("Insert Lock", "הוסף נעילה"), ("Refresh", ""), ("ID does not exist", ""), @@ -653,6 +653,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), - ("Insert Ctrl + Alt + Del", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hr.rs b/src/lang/hr.rs index ce591f58bcae..4a3136c833f6 100644 --- a/src/lang/hr.rs +++ b/src/lang/hr.rs @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "Prikaži kvalitetu monitora"), ("Disable clipboard", "Zabrani međuspremnik"), ("Lock after session end", "Zaključaj po završetku sesije"), - ("Insert", "Umetni"), + ("Insert Ctrl + Alt + Del", "Ctrl + Alt + Del umetanje"), ("Insert Lock", "Zaključaj umetanje"), ("Refresh", "Osvježi"), ("ID does not exist", "ID ne postoji"), @@ -653,6 +653,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), - ("Insert Ctrl + Alt + Del", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 20c509594569..f1f8ac1ae04a 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", ""), ("Disable clipboard", "Közös vágólap kikapcsolása"), ("Lock after session end", "Távoli fiók zárolása a munkamenet végén"), - ("Insert", ""), + ("Insert Ctrl + Alt + Del", "Illessze be a Ctrl + Alt + Del"), ("Insert Lock", "Távoli fiók zárolása"), ("Refresh", "Frissítés"), ("ID does not exist", "Az azonosító nem létezik"), @@ -653,6 +653,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), - ("Insert Ctrl + Alt + Del", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index 25fa0e5dbe8a..066f2980cc18 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "Tampilkan kualitas monitor"), ("Disable clipboard", "Matikan papan klip"), ("Lock after session end", "Kunci setelah sesi berakhir"), - ("Insert", "Menyisipkan"), + ("Insert Ctrl + Alt + Del", "Menyisipkan Ctrl + Alt + Del"), ("Insert Lock", "Masukkan Kunci"), ("Refresh", "Segarkan"), ("ID does not exist", "ID tidak ada"), @@ -653,6 +653,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), - ("Insert Ctrl + Alt + Del", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index 4f47b4aac18f..dd944af9f4d3 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "Visualizza qualità video"), ("Disable clipboard", "Disabilita appunti"), ("Lock after session end", "Blocca al termine della sessione"), - ("Insert", "Inserisci"), + ("Insert Ctrl + Alt + Del", "Ctrl + Alt + Del inserimento"), ("Insert Lock", "Blocco inserimento"), ("Refresh", "Aggiorna"), ("ID does not exist", "L'ID non esiste"), @@ -653,6 +653,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", "Cartella upload"), ("Upload files", "File upload"), ("Clipboard is synchronized", "Gli appunti sono sincronizzati"), - ("Insert Ctrl + Alt + Del", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 5934683b4a3b..5edd50572994 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "品質モニターを表示"), ("Disable clipboard", "クリップボードを無効化"), ("Lock after session end", "セッション終了後にロックする"), - ("Insert", "送信"), + ("Insert Ctrl + Alt + Del", "Ctrl + Alt + Del 送信"), ("Insert Lock", "ロック命令を送信"), ("Refresh", "更新"), ("ID does not exist", "IDが存在しません"), @@ -653,6 +653,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), - ("Insert Ctrl + Alt + Del", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 036af2a4d536..71bff5119085 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "품질 모니터 보기"), ("Disable clipboard", "클립보드 비활성화"), ("Lock after session end", "세션 종료 후 화면 잠금"), - ("Insert", "입력"), + ("Insert Ctrl + Alt + Del", "Ctrl + Alt + Del 입력"), ("Insert Lock", "원격 입력 잠금"), ("Refresh", "새로고침"), ("ID does not exist", "ID가 존재하지 않습니다"), @@ -653,6 +653,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), - ("Insert Ctrl + Alt + Del", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index 874ecfc57308..07ca645f2728 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "Сапа мониторын көрсету"), ("Disable clipboard", "Көшіру-тақтасын өшіру"), ("Lock after session end", "Сеш аяқталған соң құлыптау"), - ("Insert", "Кірістіру"), + ("Insert Ctrl + Alt + Del", "Кірістіру Ctrl + Alt + Del"), ("Insert Lock", "Кірістіруді Құлыптау"), ("Refresh", "Жаңарту"), ("ID does not exist", "ID табылмады"), @@ -653,6 +653,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), - ("Insert Ctrl + Alt + Del", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lt.rs b/src/lang/lt.rs index 7f5b7b023317..9a2069163ed4 100644 --- a/src/lang/lt.rs +++ b/src/lang/lt.rs @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "Rodyti kokybės monitorių"), ("Disable clipboard", "Išjungti mainų sritį"), ("Lock after session end", "Užrakinti pasibaigus seansui"), - ("Insert", "Įdėti"), + ("Insert Ctrl + Alt + Del", "Įdėti Ctrl + Alt + Del"), ("Insert Lock", "Įterpti užraktą"), ("Refresh", "Atnaujinti"), ("ID does not exist", "ID neegzistuoja"), @@ -653,6 +653,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), - ("Insert Ctrl + Alt + Del", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lv.rs b/src/lang/lv.rs index 0a89e02e2cfc..e8ba903dfb66 100644 --- a/src/lang/lv.rs +++ b/src/lang/lv.rs @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "Rādīt kvalitātes monitoru"), ("Disable clipboard", "Atspējot starpliktuvi"), ("Lock after session end", "Bloķēt pēc sesijas beigām"), - ("Insert", "Ievietot"), + ("Insert Ctrl + Alt + Del", "Ievietot Ctrl + Alt + Del"), ("Insert Lock", "Ievietot Bloķēt"), ("Refresh", "Atsvaidzināt"), ("ID does not exist", "ID neeksistē"), @@ -653,6 +653,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", "Augšupielādēt mapi"), ("Upload files", "Augšupielādēt failus"), ("Clipboard is synchronized", "Starpliktuve ir sinhronizēta"), - ("Insert Ctrl + Alt + Del", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nb.rs b/src/lang/nb.rs index b07a103c15f7..a91e31e45bef 100644 --- a/src/lang/nb.rs +++ b/src/lang/nb.rs @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "Vis bildekvalitet"), ("Disable clipboard", "Deaktiver utklipstavle"), ("Lock after session end", "Lås etter avsluttet fjernstyring"), - ("Insert", "Sett inn"), + ("Insert Ctrl + Alt + Del", "Sett inn Ctrl + Alt + Del"), ("Insert Lock", "Sett inn lås"), ("Refresh", "Oppdater"), ("ID does not exist", "ID finnes ikke"), @@ -653,6 +653,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), - ("Insert Ctrl + Alt + Del", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nl.rs b/src/lang/nl.rs index 71384cee002e..ca46a32853e7 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "Kwaliteitsmonitor tonen"), ("Disable clipboard", "Klembord uitschakelen"), ("Lock after session end", "Vergrendelen na einde sessie"), - ("Insert", "Invoegen"), + ("Insert Ctrl + Alt + Del", "Ctrl + Alt + Del Invoegen"), ("Insert Lock", "Vergrendeling Invoegen"), ("Refresh", "Vernieuwen"), ("ID does not exist", "ID bestaat niet"), @@ -653,6 +653,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", "Map uploaden"), ("Upload files", "Bestanden uploaden"), ("Clipboard is synchronized", "Klembord is gesynchroniseerd"), - ("Insert Ctrl + Alt + Del", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index f6f4db4f7ecb..fd5641ac133e 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "Parametry połączenia"), ("Disable clipboard", "Wyłącz schowek"), ("Lock after session end", "Zablokuj po zakończeniu sesji"), - ("Insert", "Wyślij"), + ("Insert Ctrl + Alt + Del", "Wyślij Ctrl + Alt + Del"), ("Insert Lock", "Zablokuj zdalne urządzenie"), ("Refresh", "Odśwież"), ("ID does not exist", "ID nie istnieje"), @@ -653,6 +653,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", "Wyślij folder"), ("Upload files", "Wyślij pliki"), ("Clipboard is synchronized", "Schowek jest zsynchronizowany"), - ("Insert Ctrl + Alt + Del", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index 80676744855c..c0564e0f4014 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", ""), ("Disable clipboard", "Desabilitar área de transferência"), ("Lock after session end", "Bloquear após o fim da sessão"), - ("Insert", "Inserir"), + ("Insert Ctrl + Alt + Del", "Ctrl + Alt + Del Inserir"), ("Insert Lock", "Bloquear Inserir"), ("Refresh", "Actualizar"), ("ID does not exist", "ID não existente"), @@ -653,6 +653,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), - ("Insert Ctrl + Alt + Del", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index 8d83ea394b25..14254388cc11 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "Exibir monitor de qualidade"), ("Disable clipboard", "Desabilitar área de transferência"), ("Lock after session end", "Bloquear após o fim da sessão"), - ("Insert", "Inserir"), + ("Insert Ctrl + Alt + Del", "Ctrl + Alt + Del Inserir"), ("Insert Lock", "Bloquear computador"), ("Refresh", "Atualizar"), ("ID does not exist", "ID não existe"), @@ -653,6 +653,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), - ("Insert Ctrl + Alt + Del", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ro.rs b/src/lang/ro.rs index 7c8ea1d1e861..cbce2f2a9278 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "Afișează detalii despre conexiune"), ("Disable clipboard", "Dezactivează clipboard"), ("Lock after session end", "Blochează după deconectare"), - ("Insert", "Introdu"), + ("Insert Ctrl + Alt + Del", "Introdu Ctrl + Alt + Del"), ("Insert Lock", "Blochează computer"), ("Refresh", "Reîmprospătează"), ("ID does not exist", "ID neexistent"), @@ -653,6 +653,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), - ("Insert Ctrl + Alt + Del", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index bdf4ba462568..3fb856098c71 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "Показывать монитор качества"), ("Disable clipboard", "Отключить буфер обмена"), ("Lock after session end", "Заблокировать учётную запись после сеанса"), - ("Insert", "Вставить"), + ("Insert Ctrl + Alt + Del", "Вставить Ctrl + Alt + Del"), ("Insert Lock", "Заблокировать учётную запись"), ("Refresh", "Обновить"), ("ID does not exist", "ID не существует"), @@ -653,6 +653,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", "Загрузить папку"), ("Upload files", "Загрузить файлы"), ("Clipboard is synchronized", "Буфер обмена синхронизирован"), - ("Insert Ctrl + Alt + Del", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 01d2a7507cd9..b3c8fddf9162 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "Zobraziť monitor kvality"), ("Disable clipboard", "Vypnúť schránku"), ("Lock after session end", "Po skončení uzamknúť plochu"), - ("Insert", "Vložiť"), + ("Insert Ctrl + Alt + Del", "Vložiť Ctrl + Alt + Del"), ("Insert Lock", "Uzamknúť"), ("Refresh", "Aktualizovať"), ("ID does not exist", "ID neexistuje"), @@ -653,6 +653,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), - ("Insert Ctrl + Alt + Del", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sl.rs b/src/lang/sl.rs index ba20e0148c0c..20fd24c9ca0b 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "Prikaži nadzornik kakovosti"), ("Disable clipboard", "Onemogoči odložišče"), ("Lock after session end", "Zakleni ob koncu seje"), - ("Insert", "Vstavi"), + ("Insert Ctrl + Alt + Del", "Vstavi Ctrl + Alt + Del"), ("Insert Lock", "Zakleni oddaljeni računalnik"), ("Refresh", "Osveži"), ("ID does not exist", "ID ne obstaja"), @@ -653,6 +653,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), - ("Insert Ctrl + Alt + Del", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sq.rs b/src/lang/sq.rs index fe398bc59945..7c63c8ea5ab7 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "Shaq cilësinë e monitorit"), ("Disable clipboard", "Ç'aktivizo clipboard"), ("Lock after session end", "Kyç pasi sesioni të përfundoj"), - ("Insert", "Fut"), + ("Insert Ctrl + Alt + Del", "Fut Ctrl + Alt + Del"), ("Insert Lock", "Fut bllokimin"), ("Refresh", "Rifresko"), ("ID does not exist", "ID nuk ekziston"), @@ -653,6 +653,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), - ("Insert Ctrl + Alt + Del", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sr.rs b/src/lang/sr.rs index cca6bb361add..e80bb61812f4 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "Prikaži monitor kvaliteta"), ("Disable clipboard", "Zabrani clipboard"), ("Lock after session end", "Zaključaj po završetku sesije"), - ("Insert", "Umetni"), + ("Insert Ctrl + Alt + Del", "Ctrl + Alt + Del umetanje"), ("Insert Lock", "Zaključaj umetanje"), ("Refresh", "Osveži"), ("ID does not exist", "ID ne postoji"), @@ -653,6 +653,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), - ("Insert Ctrl + Alt + Del", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sv.rs b/src/lang/sv.rs index 0cb2f92c8f2d..dae48e7a368c 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "Visa bildkvalitet"), ("Disable clipboard", "Stäng av urklipp"), ("Lock after session end", "Lås efter sessionens slut"), - ("Insert", "Insert"), + ("Insert Ctrl + Alt + Del", "Insert Ctrl + Alt + Del"), ("Insert Lock", "Insert lås"), ("Refresh", "Uppdatera"), ("ID does not exist", "Detta ID existerar inte"), @@ -653,6 +653,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), - ("Insert Ctrl + Alt + Del", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index b015e177dbcc..60b281851371 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", ""), ("Disable clipboard", ""), ("Lock after session end", ""), - ("Insert", ""), + ("Insert Ctrl + Alt + Del", ""), ("Insert Lock", ""), ("Refresh", ""), ("ID does not exist", ""), @@ -653,6 +653,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), - ("Insert Ctrl + Alt + Del", ""), ].iter().cloned().collect(); } diff --git a/src/lang/th.rs b/src/lang/th.rs index 8e0711525ef0..71af446c144e 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "แสดงคุณภาพหน้าจอ"), ("Disable clipboard", "ปิดการใช้งานคลิปบอร์ด"), ("Lock after session end", "ล็อคหลังจากจบเซสชัน"), - ("Insert", "แทรก"), + ("Insert Ctrl + Alt + Del", "แทรก Ctrl + Alt + Del"), ("Insert Lock", "แทรกล็อค"), ("Refresh", "รีเฟรช"), ("ID does not exist", "ไม่พอข้อมูล ID"), @@ -653,6 +653,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), - ("Insert Ctrl + Alt + Del", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index 2d1be69d3ad5..ce11544b5094 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "Kalite monitörünü göster"), ("Disable clipboard", "Hafızadaki kopyalanmışları engelle"), ("Lock after session end", "Bağlantıdan sonra kilitle"), - ("Insert", "Ekle"), + ("Insert Ctrl + Alt + Del", "Ctrl + Alt + Del Ekle"), ("Insert Lock", "Kilit Ekle"), ("Refresh", "Yenile"), ("ID does not exist", "ID bulunamadı"), @@ -653,6 +653,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), - ("Insert Ctrl + Alt + Del", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 8ac7c13ff412..b0f64f82d397 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "顯示品質監測"), ("Disable clipboard", "停用剪貼簿"), ("Lock after session end", "工作階段結束後鎖定電腦"), - ("Insert", "插入"), + ("Insert Ctrl + Alt + Del", "插入 Ctrl + Alt + Del"), ("Insert Lock", "鎖定遠端電腦"), ("Refresh", "重新載入"), ("ID does not exist", "ID 不存在"), @@ -653,6 +653,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", "上傳資料夾"), ("Upload files", "上傳檔案"), ("Clipboard is synchronized", ""), - ("Insert Ctrl + Alt + Del", ""), ].iter().cloned().collect(); } diff --git a/src/lang/uk.rs b/src/lang/uk.rs index 63e3cc199a3f..ff5c8b64ae72 100644 --- a/src/lang/uk.rs +++ b/src/lang/uk.rs @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "Показати якість"), ("Disable clipboard", "Вимкнути буфер обміну"), ("Lock after session end", "Блокування після завершення сеансу"), - ("Insert", "Вставити"), + ("Insert Ctrl + Alt + Del", "Вставити Ctrl + Alt + Del"), ("Insert Lock", "Встановити замок"), ("Refresh", "Оновити"), ("ID does not exist", "ID не існує"), @@ -653,6 +653,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), - ("Insert Ctrl + Alt + Del", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index 1454981ac00e..5a2c47befcc7 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "Hiện thị chất lượng của màn hình"), ("Disable clipboard", "Tắt clipboard"), ("Lock after session end", "Khóa sau khi kết thúc phiên kết nối"), - ("Insert", "Cài"), + ("Insert Ctrl + Alt + Del", "Cài Ctrl + Alt + Del"), ("Insert Lock", "Cài khóa"), ("Refresh", "Làm mới"), ("ID does not exist", "ID không tồn tại"), @@ -653,6 +653,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), - ("Insert Ctrl + Alt + Del", ""), ].iter().cloned().collect(); } From 4da584055d791d6533ff0376d6216a2df1c175e2 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Thu, 24 Oct 2024 22:52:06 +0800 Subject: [PATCH 321/541] fix ci --- flutter/lib/desktop/widgets/remote_toolbar.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/lib/desktop/widgets/remote_toolbar.dart b/flutter/lib/desktop/widgets/remote_toolbar.dart index 4857464861fc..839ea1a81db2 100644 --- a/flutter/lib/desktop/widgets/remote_toolbar.dart +++ b/flutter/lib/desktop/widgets/remote_toolbar.dart @@ -305,7 +305,7 @@ class RemoteMenuEntry { }) { return MenuEntryButton( childBuilder: (TextStyle? style) => Text( - '${translate("Insert Ctrl + Alt + Del")', + translate("Insert Ctrl + Alt + Del"), style: style, ), proc: () { From c8b90319966a54ea5629344d328c54701f671a08 Mon Sep 17 00:00:00 2001 From: 21pages Date: Thu, 24 Oct 2024 23:14:43 +0800 Subject: [PATCH 322/541] refactor session insert, update if already exists (#9739) * All share the same last_receive_time * Not second port forward Signed-off-by: 21pages --- src/server/connection.rs | 130 ++++++++++++++++++--------------------- 1 file changed, 61 insertions(+), 69 deletions(-) diff --git a/src/server/connection.rs b/src/server/connection.rs index dc184ceac8e0..12157ddbbcc4 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -152,8 +152,6 @@ struct Session { last_recv_time: Arc>, random_password: String, tfa: bool, - conn_type: AuthConnType, - conn_id: i32, } #[cfg(not(any(target_os = "android", target_os = "ios")))] @@ -217,7 +215,7 @@ pub struct Connection { server_audit_conn: String, server_audit_file: String, lr: LoginRequest, - last_recv_time: Arc>, + session_last_recv_time: Option>>, chat_unanswered: bool, file_transferred: bool, #[cfg(windows)] @@ -364,7 +362,7 @@ impl Connection { server_audit_conn: "".to_owned(), server_audit_file: "".to_owned(), lr: Default::default(), - last_recv_time: Arc::new(Mutex::new(Instant::now())), + session_last_recv_time: None, chat_unanswered: false, file_transferred: false, #[cfg(windows)] @@ -595,7 +593,7 @@ impl Connection { }, Ok(bytes) => { last_recv_time = Instant::now(); - *conn.last_recv_time.lock().unwrap() = Instant::now(); + conn.session_last_recv_time.as_mut().map(|t| *t.lock().unwrap() = Instant::now()); if let Ok(msg_in) = Message::parse_from_bytes(&bytes) { if !conn.on_message(msg_in).await { break; @@ -762,6 +760,10 @@ impl Connection { } if let Err(err) = conn.try_port_forward_loop(&mut rx_from_cm).await { conn.on_close(&err.to_string(), false).await; + raii::AuthedConnID::remove_session_if_last_duplication( + conn.inner.id(), + conn.session_key(), + ); } conn.post_conn_audit(json!({ @@ -1140,6 +1142,11 @@ impl Connection { auth_conn_type, self.session_key(), )); + self.session_last_recv_time = SESSIONS + .lock() + .unwrap() + .get(&self.session_key()) + .map(|s| s.last_recv_time.clone()); self.post_conn_audit( json!({"peer": ((&self.lr.my_id, &self.lr.my_name)), "type": conn_type}), ); @@ -1549,15 +1556,10 @@ impl Connection { if password::temporary_enabled() { let password = password::temporary_password(); if self.validate_one_password(password.clone()) { - raii::AuthedConnID::insert_session( + raii::AuthedConnID::update_or_insert_session( self.session_key(), - Session { - last_recv_time: self.last_recv_time.clone(), - random_password: password, - tfa: false, - conn_type: self.conn_type(), - conn_id: self.inner.id(), - }, + Some(password), + Some(false), ); return true; } @@ -1581,15 +1583,11 @@ impl Connection { .get(&self.session_key()) .map(|s| s.to_owned()); // last_recv_time is a mutex variable shared with connection, can be updated lively. - if let Some(mut session) = session { + if let Some(session) = session { if !self.lr.password.is_empty() && (tfa && session.tfa || !tfa && self.validate_one_password(session.random_password.clone())) { - session.last_recv_time = self.last_recv_time.clone(); - session.conn_id = self.inner.id(); - session.conn_type = self.conn_type(); - raii::AuthedConnID::insert_session(self.session_key(), session); log::info!("is recent session"); return true; } @@ -1841,34 +1839,13 @@ impl Connection { if res { self.update_failure(failure, true, 1); self.require_2fa.take(); + raii::AuthedConnID::set_session_2fa(self.session_key()); self.send_logon_response().await; self.try_start_cm( self.lr.my_id.to_owned(), self.lr.my_name.to_owned(), self.authorized, ); - let session = SESSIONS - .lock() - .unwrap() - .get(&self.session_key()) - .map(|s| s.to_owned()); - if let Some(mut session) = session { - session.tfa = true; - session.conn_id = self.inner.id(); - session.conn_type = self.conn_type(); - raii::AuthedConnID::insert_session(self.session_key(), session); - } else { - raii::AuthedConnID::insert_session( - self.session_key(), - Session { - last_recv_time: self.last_recv_time.clone(), - random_password: "".to_owned(), - tfa: true, - conn_type: self.conn_type(), - conn_id: self.inner.id(), - }, - ); - } if !tfa.hwid.is_empty() && Self::enable_trusted_devices() { Config::add_trusted_device(TrustedDevice { hwid: tfa.hwid, @@ -3872,16 +3849,17 @@ mod raii { } pub fn remove_session_if_last_duplication(conn_id: i32, key: SessionKey) { - let contains = SESSIONS.lock().unwrap().contains_key(&key); + let mut lock = SESSIONS.lock().unwrap(); + let contains = lock.contains_key(&key); if contains { let another = AUTHED_CONNS .lock() .unwrap() .iter() - .any(|c| c.0 != conn_id && c.2 == key && c.1 != AuthConnType::PortForward); + .any(|c| c.0 != conn_id && c.2 == key); if !another { // Keep the session if there is another connection with same peer_id and session_id. - SESSIONS.lock().unwrap().remove(&key); + lock.remove(&key); log::info!("remove session"); } else { log::info!("skip remove session"); @@ -3889,32 +3867,46 @@ mod raii { } } - pub fn insert_session(key: SessionKey, session: Session) { - let mut insert = true; - if session.conn_type == AuthConnType::PortForward { - // port forward doesn't update last received time - let other_alive_conns = AUTHED_CONNS - .lock() - .unwrap() - .iter() - .filter(|c| { - c.2 == key && c.1 != AuthConnType::PortForward // port forward doesn't remove itself - }) - .map(|c| c.0) - .collect::>(); - let another = SESSIONS.lock().unwrap().get(&key).map(|s| { - other_alive_conns.contains(&s.conn_id) - && s.tfa == session.tfa - && s.conn_type != AuthConnType::PortForward - }) == Some(true); - if another { - insert = false; - log::info!("skip insert session for port forward"); - } - } - if insert { - log::info!("insert session for {:?}", session.conn_type); - SESSIONS.lock().unwrap().insert(key, session); + pub fn update_or_insert_session( + key: SessionKey, + password: Option, + tfa: Option, + ) { + let mut lock = SESSIONS.lock().unwrap(); + let session = lock.get_mut(&key); + if let Some(session) = session { + if let Some(password) = password { + session.random_password = password; + } + if let Some(tfa) = tfa { + session.tfa = tfa; + } + } else { + lock.insert( + key, + Session { + random_password: password.unwrap_or_default(), + tfa: tfa.unwrap_or_default(), + last_recv_time: Arc::new(Mutex::new(Instant::now())), + }, + ); + } + } + + pub fn set_session_2fa(key: SessionKey) { + let mut lock = SESSIONS.lock().unwrap(); + let session = lock.get_mut(&key); + if let Some(session) = session { + session.tfa = true; + } else { + lock.insert( + key, + Session { + last_recv_time: Arc::new(Mutex::new(Instant::now())), + random_password: "".to_owned(), + tfa: true, + }, + ); } } } From c51771c8548ddd0970337bb6a9c2fcbc0b9bb677 Mon Sep 17 00:00:00 2001 From: solokot Date: Fri, 25 Oct 2024 03:08:02 +0300 Subject: [PATCH 323/541] Update ru.rs (#9741) --- src/lang/ru.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 3fb856098c71..6d173f1097b8 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -364,7 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recording", "Запись"), ("Directory", "Папка"), ("Automatically record incoming sessions", "Автоматически записывать входящие сеансы"), - ("Automatically record outgoing sessions", ""), + ("Automatically record outgoing sessions", "Автоматически записывать исходящие сеансы"), ("Change", "Изменить"), ("Start session recording", "Начать запись сеанса"), ("Stop session recording", "Остановить запись сеанса"), From 924aa515c6977a6f4c31078fe6a86e2db61ab237 Mon Sep 17 00:00:00 2001 From: Tobias Degen <40177712+tobiasdegen@users.noreply.github.com> Date: Fri, 25 Oct 2024 02:21:42 +0200 Subject: [PATCH 324/541] fix german translation (#9742) --- src/lang/de.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lang/de.rs b/src/lang/de.rs index e59c3b5050ca..a732213712ab 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "Qualitätsüberwachung anzeigen"), ("Disable clipboard", "Zwischenablage deaktivieren"), ("Lock after session end", "Nach Sitzungsende sperren"), - ("Insert Ctrl + Alt + Del", "Einfügen Ctrl + Alt + Del"), + ("Insert Ctrl + Alt + Del", "Strg + Alt + Entf senden"), ("Insert Lock", "Win+L (Sperren) senden"), ("Refresh", "Aktualisieren"), ("ID does not exist", "Diese ID existiert nicht."), From 129f6c869b2d836b829fc5939f4d783f6de08f45 Mon Sep 17 00:00:00 2001 From: bovirus <1262554+bovirus@users.noreply.github.com> Date: Sat, 26 Oct 2024 10:06:29 +0200 Subject: [PATCH 325/541] Update Italian language (#9752) --- src/lang/it.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lang/it.rs b/src/lang/it.rs index dd944af9f4d3..e0dd83db6f7a 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "Visualizza qualità video"), ("Disable clipboard", "Disabilita appunti"), ("Lock after session end", "Blocca al termine della sessione"), - ("Insert Ctrl + Alt + Del", "Ctrl + Alt + Del inserimento"), + ("Insert Ctrl + Alt + Del", "Inserisci Ctrl + Alt + Del"), ("Insert Lock", "Blocco inserimento"), ("Refresh", "Aggiorna"), ("ID does not exist", "L'ID non esiste"), From 40e8f0d3076426f6bb5780f0bf21bd2509f5c17a Mon Sep 17 00:00:00 2001 From: 21pages Date: Sat, 26 Oct 2024 22:05:54 +0800 Subject: [PATCH 326/541] revert missing retry and opt keep session (#9755) * Revert "fix missing retry (#8750)" If `hasRetry` is true: there is a retry timeout; If `hasRetry` is false: there is no retry button; In https://github.com/rustdesk/rustdesk/discussions/8748#discussioncomment-10081038,when doesn't want inactive to retry, https://github.com/rustdesk/rustdesk/blob/cf0e3ec303990a48e0b3a6beedd3587079a6526c/flutter/lib/models/model.dart#L444, 1.2.3 always show retry no matter what `hasRetry` is. This reverts commit c3c99ba10725158eaf37fb7fbf7665526138bb88. * not keep session if there is no remote connection left. Signed-off-by: 21pages --------- Signed-off-by: 21pages --- flutter/lib/common.dart | 38 ++++++++++++----------------------- flutter/lib/models/model.dart | 2 +- src/server/connection.rs | 34 +++++++++++-------------------- 3 files changed, 26 insertions(+), 48 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 099a04a1569e..a2ad96775460 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -1174,33 +1174,21 @@ void msgBox(SessionID sessionId, String type, String title, String text, dialogManager.dismissAll(); })); } - if (reconnect != null && title == "Connection Error") { + if (reconnect != null && + title == "Connection Error" && + reconnectTimeout != null) { // `enabled` is used to disable the dialog button once the button is clicked. final enabled = true.obs; - final button = reconnectTimeout != null - ? Obx(() => _ReconnectCountDownButton( - second: reconnectTimeout, - onPressed: enabled.isTrue - ? () { - // Disable the button - enabled.value = false; - reconnect(dialogManager, sessionId, false); - } - : null, - )) - : Obx( - () => dialogButton( - 'Reconnect', - isOutline: true, - onPressed: enabled.isTrue - ? () { - // Disable the button - enabled.value = false; - reconnect(dialogManager, sessionId, false); - } - : null, - ), - ); + final button = Obx(() => _ReconnectCountDownButton( + second: reconnectTimeout, + onPressed: enabled.isTrue + ? () { + // Disable the button + enabled.value = false; + reconnect(dialogManager, sessionId, false); + } + : null, + )); buttons.insert(0, button); } if (link.isNotEmpty) { diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 8bd0530f13cc..bd91949843b3 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -638,7 +638,7 @@ class FfiModel with ChangeNotifier { {bool? hasCancel}) { msgBox(sessionId, type, title, text, link, dialogManager, hasCancel: hasCancel, - reconnect: reconnect, + reconnect: hasRetry ? reconnect : null, reconnectTimeout: hasRetry ? _reconnects : null); _timer?.cancel(); if (hasRetry) { diff --git a/src/server/connection.rs b/src/server/connection.rs index 12157ddbbcc4..c0cf8c784e68 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -760,10 +760,7 @@ impl Connection { } if let Err(err) = conn.try_port_forward_loop(&mut rx_from_cm).await { conn.on_close(&err.to_string(), false).await; - raii::AuthedConnID::remove_session_if_last_duplication( - conn.inner.id(), - conn.session_key(), - ); + raii::AuthedConnID::check_remove_session(conn.inner.id(), conn.session_key()); } conn.post_conn_audit(json!({ @@ -2377,7 +2374,7 @@ impl Connection { } Some(misc::Union::CloseReason(_)) => { self.on_close("Peer close", true).await; - raii::AuthedConnID::remove_session_if_last_duplication( + raii::AuthedConnID::check_remove_session( self.inner.id(), self.session_key(), ); @@ -3140,7 +3137,7 @@ impl Connection { let mut msg_out = Message::new(); msg_out.set_misc(misc); self.send(msg_out).await; - raii::AuthedConnID::remove_session_if_last_duplication(self.inner.id(), self.session_key()); + raii::AuthedConnID::check_remove_session(self.inner.id(), self.session_key()); } fn read_dir(&mut self, dir: &str, include_hidden: bool) { @@ -3295,17 +3292,6 @@ impl Connection { } } - #[inline] - fn conn_type(&self) -> AuthConnType { - if self.file_transfer.is_some() { - AuthConnType::FileTransfer - } else if self.port_forward_socket.is_some() { - AuthConnType::PortForward - } else { - AuthConnType::Remote - } - } - #[inline] fn session_key(&self) -> SessionKey { SessionKey { @@ -3848,20 +3834,24 @@ mod raii { .count() } - pub fn remove_session_if_last_duplication(conn_id: i32, key: SessionKey) { + pub fn check_remove_session(conn_id: i32, key: SessionKey) { let mut lock = SESSIONS.lock().unwrap(); let contains = lock.contains_key(&key); if contains { - let another = AUTHED_CONNS + // If there are 2 connections with the same peer_id and session_id, a remote connection and a file transfer or port forward connection, + // If any of the connections is closed allowing retry, this will not be called; + // If the file transfer/port forward connection is closed with no retry, the session should be kept for remote control menu action; + // If the remote connection is closed with no retry, keep the session is not reasonable in case there is a retry button in the remote side, and ignore network fluctuations. + let another_remote = AUTHED_CONNS .lock() .unwrap() .iter() - .any(|c| c.0 != conn_id && c.2 == key); - if !another { - // Keep the session if there is another connection with same peer_id and session_id. + .any(|c| c.0 != conn_id && c.2 == key && c.1 == AuthConnType::Remote); + if !another_remote { lock.remove(&key); log::info!("remove session"); } else { + // Keep the session if there is another remote connection with same peer_id and session_id. log::info!("skip remove session"); } } From c56584906276463f7a4c23e26fc6a1ad1da0252b Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Sun, 27 Oct 2024 20:36:21 +0800 Subject: [PATCH 327/541] fix: Function "LockScreen" on macOS since "ignore_flags" in enigo is introduced. (#9757) 1. LockScreen after connection is established. 2. LockScreen after "Map mode" or "Translate mode" keys are sent. Signed-off-by: fufesou --- src/server/input_service.rs | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/src/server/input_service.rs b/src/server/input_service.rs index 3189520be0a5..c7f651e9ac72 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -456,13 +456,22 @@ static RECORD_CURSOR_POS_RUNNING: AtomicBool = AtomicBool::new(false); // https://github.com/rustdesk/rustdesk/issues/9729 // We need to do some special handling for macOS when using the legacy mode. #[cfg(target_os = "macos")] -static LAST_KEY_LEGACY_MODE: AtomicBool = AtomicBool::new(false); -// We use enigo to simulate mouse events. Only the legacy mode uses the key flags. +static LAST_KEY_LEGACY_MODE: AtomicBool = AtomicBool::new(true); +// We use enigo to +// 1. Simulate mouse events +// 2. Simulate the legacy mode key events +// 3. Simulate the functioin key events, like LockScreen #[inline] #[cfg(target_os = "macos")] fn enigo_ignore_flags() -> bool { !LAST_KEY_LEGACY_MODE.load(Ordering::SeqCst) } +#[inline] +#[cfg(target_os = "macos")] +fn set_last_legacy_mode(v: bool) { + LAST_KEY_LEGACY_MODE.store(v, Ordering::SeqCst); + ENIGO.lock().unwrap().set_ignore_flags(!v); +} pub fn try_start_record_cursor_pos() -> Option> { if RECORD_CURSOR_POS_RUNNING.load(Ordering::SeqCst) { @@ -1698,17 +1707,19 @@ pub fn handle_key_(evt: &KeyEvent) { match evt.mode.enum_value() { Ok(KeyboardMode::Map) => { #[cfg(target_os = "macos")] - LAST_KEY_LEGACY_MODE.store(false, Ordering::SeqCst); + set_last_legacy_mode(false); map_keyboard_mode(evt); } Ok(KeyboardMode::Translate) => { #[cfg(target_os = "macos")] - LAST_KEY_LEGACY_MODE.store(false, Ordering::SeqCst); + set_last_legacy_mode(false); translate_keyboard_mode(evt); } _ => { + // All key down events are started from here, + // so we can reset the flag of last legacy mode here. #[cfg(target_os = "macos")] - LAST_KEY_LEGACY_MODE.store(true, Ordering::SeqCst); + set_last_legacy_mode(true); legacy_keyboard_mode(evt); } } From 3a7594755341f023f56fa4b6a43b60d6b47df88d Mon Sep 17 00:00:00 2001 From: 21pages Date: Mon, 28 Oct 2024 09:40:16 +0800 Subject: [PATCH 328/541] add some missing web bridge (#9763) Signed-off-by: 21pages --- flutter/lib/web/bridge.dart | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/flutter/lib/web/bridge.dart b/flutter/lib/web/bridge.dart index d38f0f9cf393..20891281455d 100644 --- a/flutter/lib/web/bridge.dart +++ b/flutter/lib/web/bridge.dart @@ -82,6 +82,7 @@ class RustdeskImpl { required bool forceRelay, required String password, required bool isSharedPassword, + String? connToken, dynamic hint}) { return js.context.callMethod('setByName', [ 'session_add_sync', @@ -173,18 +174,12 @@ class RustdeskImpl { } Future sessionRecordScreen( - {required UuidValue sessionId, - required bool start, - required int display, - required int width, - required int height, - dynamic hint}) { + {required UuidValue sessionId, required bool start, dynamic hint}) { throw UnimplementedError("sessionRecordScreen"); } - Future sessionRecordStatus( - {required UuidValue sessionId, required bool status, dynamic hint}) { - throw UnimplementedError("sessionRecordStatus"); + bool sessionGetIsRecording({required UuidValue sessionId, dynamic hint}) { + return false; } Future sessionReconnect( @@ -707,7 +702,8 @@ class RustdeskImpl { Future sessionSendSelectedSessionId( {required UuidValue sessionId, required String sid, dynamic hint}) { - throw UnimplementedError("sessionSendSelectedSessionId"); + return Future( + () => js.context.callMethod('setByName', ['selected_sid', sid])); } Future> mainGetSoundInputs({dynamic hint}) { @@ -1828,5 +1824,9 @@ class RustdeskImpl { return Future(() => js.context.callMethod('setByName', ['select_files'])); } + String? sessionGetConnToken({required UuidValue sessionId, dynamic hint}) { + throw UnimplementedError("sessionGetConnToken"); + } + void dispose() {} } From f0450db2030f71df423b5c508b98b7332de0a355 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Tue, 29 Oct 2024 09:49:04 +0800 Subject: [PATCH 329/541] refact: mobile reset canvas (#9766) Signed-off-by: fufesou --- flutter/lib/models/model.dart | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index bd91949843b3..ecbfd6fa4398 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -1659,11 +1659,25 @@ class CanvasModel with ChangeNotifier { notifyListeners(); } - clear([bool notify = false]) { + // For reset canvas to the last view style + reset() { + _scale = _lastViewStyle.scale; + _devicePixelRatio = ui.window.devicePixelRatio; + if (kIgnoreDpi && _lastViewStyle.style == kRemoteViewStyleOriginal) { + _scale = 1.0 / _devicePixelRatio; + } + final displayWidth = getDisplayWidth(); + final displayHeight = getDisplayHeight(); + _x = (size.width - displayWidth * _scale) / 2; + _y = (size.height - displayHeight * _scale) / 2; + bind.sessionSetViewStyle(sessionId: sessionId, value: _lastViewStyle.style); + notifyListeners(); + } + + clear() { _x = 0; _y = 0; _scale = 1.0; - if (notify) notifyListeners(); } updateScrollPercent() { @@ -1988,7 +2002,7 @@ class CursorModel with ChangeNotifier { _x = _displayOriginX; _y = _displayOriginY; parent.target?.inputModel.moveMouse(_x, _y); - parent.target?.canvasModel.clear(true); + parent.target?.canvasModel.reset(); notifyListeners(); } From 26d23d588a8a0d6263d76193e84189411411db8f Mon Sep 17 00:00:00 2001 From: pppanghu77 Date: Tue, 29 Oct 2024 13:54:16 +0800 Subject: [PATCH 330/541] fix: [translations] Add the translation in cn.rs (#9768) Add the translation in cn.rs Log: Add the translation in cn.rs --- src/lang/cn.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 8b1d3a5f9a44..12b3a4257e9a 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -44,7 +44,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("id_change_tip", "只可以使用字母 a-z, A-Z, 0-9, _ (下划线)。首字母必须是 a-z, A-Z。长度在 6 与 16 之间。"), ("Website", "网站"), ("About", "关于"), - ("Slogan_tip", ""), + ("Slogan_tip", "在这个混乱的世界中,用心制作!"), ("Privacy Statement", "隐私声明"), ("Mute", "静音"), ("Build Date", "构建日期"), From adc5a7be51bec5a8009c0ae056e3b91e64918054 Mon Sep 17 00:00:00 2001 From: John Fowler Date: Tue, 29 Oct 2024 06:55:13 +0100 Subject: [PATCH 331/541] Update hu.rs (#9762) Making the Hungarian language file complete. Adding tips in Hungarian. --- src/lang/hu.rs | 578 ++++++++++++++++++++++++------------------------- 1 file changed, 289 insertions(+), 289 deletions(-) diff --git a/src/lang/hu.rs b/src/lang/hu.rs index f1f8ac1ae04a..c80bb654da70 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -1,7 +1,7 @@ lazy_static::lazy_static! { pub static ref T: std::collections::HashMap<&'static str, &'static str> = [ - ("Status", "Státusz"), + ("Status", "Állapot"), ("Your Desktop", "Saját asztal"), ("desk_tip", "A számítógép ezzel a jelszóval és azonosítóval érhető el távolról."), ("Password", "Jelszó"), @@ -19,16 +19,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recent sessions", "Legutóbbi munkamanetek"), ("Address book", "Címjegyzék"), ("Confirmation", "Megerősítés"), - ("TCP tunneling", "TCP tunneling"), + ("TCP tunneling", "TCP alagútépítés"), ("Remove", "Eltávolít"), ("Refresh random password", "Új véletlenszerű jelszó"), ("Set your own password", "Saját jelszó beállítása"), ("Enable keyboard/mouse", "Billentyűzet/egér engedélyezése"), ("Enable clipboard", "Megosztott vágólap engedélyezése"), ("Enable file transfer", "Fájlátvitel engedélyezése"), - ("Enable TCP tunneling", "TCP tunneling engedélyezése"), + ("Enable TCP tunneling", "TCP alagútépítés engedélyezése"), ("IP Whitelisting", "IP engedélyezési lista"), - ("ID/Relay Server", "ID/Relay szerver"), + ("ID/Relay Server", "ID/Továbbító szerver"), ("Import server config", "Szerver konfiguráció importálása"), ("Export Server Config", "Szerver konfiguráció exportálása"), ("Import server configuration successfully", "Szerver konfiguráció sikeresen importálva"), @@ -38,13 +38,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop service", "Szolgáltatás leállítása"), ("Change ID", "Azonosító megváltoztatása"), ("Your new ID", "Az új azonosítód"), - ("length %min% to %max%", ""), - ("starts with a letter", ""), - ("allowed characters", ""), + ("length %min% to %max%", "hossz %min% és %max% között"), + ("starts with a letter", "betűvel kezdődik"), + ("allowed characters", "engedélyezett karakterek"), ("id_change_tip", "Csak a-z, A-Z, 0-9 csoportokba tartozó karakterek, illetve a _ karakter van engedélyezve. Az első karakternek mindenképpen a-z, A-Z csoportokba kell esnie. Az azonosító hosszúsága 6-tól, 16 karakter."), - ("Website", "Weboldal"), + ("Website", "Webhely"), ("About", "Rólunk"), - ("Slogan_tip", ""), + ("Slogan_tip", "Szenvedéllyel programozva - egy káoszba süllyedő világban!"), ("Privacy Statement", "Adatvédelmi nyilatkozat"), ("Mute", "Némítás"), ("Build Date", "Build ideje"), @@ -52,7 +52,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Home", "Kezdőképernyő"), ("Audio Input", "Hangátvitel"), ("Enhancements", "Fejlesztések"), - ("Hardware Codec", "Hardware kodek"), + ("Hardware Codec", "Hardveres kódek"), ("Adaptive bitrate", "Adaptív bitráta"), ("ID Server", "ID szerver"), ("Relay Server", "Továbbító szerver"), @@ -127,7 +127,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Optimize reaction time", "Gyorsan reagáló"), ("Custom", "Egyedi"), ("Show remote cursor", "Távoli kurzor megjelenítése"), - ("Show quality monitor", ""), + ("Show quality monitor", "Minőségi monitor megjelenítése"), ("Disable clipboard", "Közös vágólap kikapcsolása"), ("Lock after session end", "Távoli fiók zárolása a munkamenet végén"), ("Insert Ctrl + Alt + Del", "Illessze be a Ctrl + Alt + Del"), @@ -143,7 +143,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Failed to connect via rendezvous server", "Nem sikerült csatlakozni a kiszolgáló szerveren keresztül"), ("Failed to connect via relay server", "Nem sikerült csatlakozni a közvetítő szerveren keresztül"), ("Failed to make direct connection to remote desktop", "Nem sikerült közvetlen kapcsolatot létesíteni a távoli számítógéppel"), - ("Set Password", "Jelszó Beállítása"), + ("Set Password", "Jelszó beállítása"), ("OS Password", "Operációs rendszer jelszavának beállítása"), ("install_tip", "Előfordul, hogy bizonyos esetekben hiba léphet fel a Portable verzió használata során. A megfelelő működés érdekében, kérem telepítse a RustDesk alkalmazást a számítógépre."), ("Click to upgrade", "Kattintson ide a frissítés telepítéséhez"), @@ -153,7 +153,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("config_acc", "A távoli vezérléshez a RustDesk-nek \"Kisegítő lehetőség\" engedélyre van szüksége"), ("config_screen", "A távoli vezérléshez szükséges a \"Képernyőfelvétel\" engedély megadása"), ("Installing ...", "Telepítés..."), - ("Install", "Telepítés"), + ("Install", "Telepítsd"), ("Installation", "Telepítés"), ("Installation Path", "Telepítési útvonal"), ("Create start menu shortcuts", "Start menü parancsikonok létrehozása"), @@ -164,7 +164,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Generating ...", "Létrehozás..."), ("Your installation is lower version.", "A telepített verzió alacsonyabb."), ("not_close_tcp_tip", "Ne zárja be ezt az ablakot miközben a tunnelt használja"), - ("Listening ...", "Keresés..."), + ("Listening ...", "Figyelés..."), ("Remote Host", "Távoli kiszolgáló"), ("Remote Port", "Távoli port"), ("Action", "Indítás"), @@ -192,7 +192,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Auto Login", "Automatikus bejelentkezés"), ("Enable direct IP access", "Közvetlen IP-elérés engedélyezése"), ("Rename", "Átnevezés"), - ("Space", ""), + ("Space", "Szóköz"), ("Create desktop shortcut", "Asztali parancsikon létrehozása"), ("Change Path", "Elérési út módosítása"), ("Create Folder", "Mappa létrehozás"), @@ -210,17 +210,17 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "A kapcsolatot a másik fél manuálisan bezárta"), ("Enable remote configuration modification", "Távoli konfiguráció módosítás engedélyezése"), ("Run without install", "Futtatás feltelepítés nélkül"), - ("Connect via relay", ""), + ("Connect via relay", "Csatlakozás közvetítőn keresztül"), ("Always connect via relay", "Mindig közvetítőn keresztüli csatlakozás"), ("whitelist_tip", "Csak az engedélyezési listán szereplő címek csatlakozhatnak"), ("Login", "Belépés"), - ("Verify", ""), - ("Remember me", ""), - ("Trust this device", ""), - ("Verification code", ""), - ("verification_tip", ""), + ("Verify", "Ellenőrzés"), + ("Remember me", "Emlékezz rám"), + ("Trust this device", "Bízzon ebben az eszközben"), + ("Verification code", "Ellenőrző kód"), + ("verification_tip", "A regisztrált e-mail címre egy ellenőrző kódot küldtek. Adja meg az ellenőrző kódot az újbóli bejelentkezéshez."), ("Logout", "Kilépés"), - ("Tags", "Tagok"), + ("Tags", "Címkék"), ("Search ID", "Azonosító keresése..."), ("whitelist_sep", "A címeket veszővel, pontosvesszővel, szóközzel, vagy új sorral válassza el"), ("Add ID", "Azonosító hozzáadása"), @@ -268,7 +268,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Note", "Megyjegyzés"), ("Connection", "Kapcsolat"), ("Share Screen", "Képernyőmegosztás"), - ("Chat", "Chat"), + ("Chat", "Csevegés"), ("Total", "Összes"), ("items", "elemek"), ("Selected", "Kijelölt"), @@ -286,8 +286,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("android_service_will_start_tip", "A \"Képernyőrögzítés\" bekapcsolásával automatikus elindul a szolgáltatás, lehetővé téve, hogy más eszközök csatlakozási kérelmet küldhessenek"), ("android_stop_service_tip", "A szolgáltatás leállítása automatikusan szétkapcsol minden létező kapcsolatot."), ("android_version_audio_tip", "A jelenlegi Android verzió nem támogatja a hangrögzítést, frissítsen legalább Android 10-re, vagy egy újabb verzióra."), - ("android_start_service_tip", ""), - ("android_permission_may_not_change_tip", ""), + ("android_start_service_tip", "A képernyőmegosztó szolgáltatás elindításához koppintson a \" Közvetítő szolgáltatás indítása\" gombra, vagy aktiválja a \"Képernyőfelvétel\" engedélyt."), + ("android_permission_may_not_change_tip", "A meglévő kapcsolatok engedélyei csak új csatlakozás után módosulnak."), ("Account", "Fiók"), ("Overwrite", "Felülírás"), ("This file exists, skip or overwrite this file?", "Ez a fájl már létezik, kihagyja vagy felülírja ezt a fájlt?"), @@ -306,11 +306,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keep RustDesk background service", "RustDesk futtatása a háttérben"), ("Ignore Battery Optimizations", "Akkumulátorkímélő figyelmen kívűl hagyása"), ("android_open_battery_optimizations_tip", "Ha le szeretné tiltani ezt a funkciót, lépjen a RustDesk alkalmazás beállítási oldalára, keresse meg az [Akkumulátorkímélő] lehetőséget és válassza a nincs korlátozás lehetőséget."), - ("Start on boot", ""), - ("Start the screen sharing service on boot, requires special permissions", ""), + ("Start on boot", "Indítás bekapcsoláskor"), + ("Start the screen sharing service on boot, requires special permissions", "Indítsa el a képernyőmegosztó szolgáltatást rendszerindításkor, speciális engedélyeket igényel"), ("Connection not allowed", "A csatlakozás nem engedélyezett"), - ("Legacy mode", ""), - ("Map mode", ""), + ("Legacy mode", "Kompatibilitási mód"), + ("Map mode", "Hozzárendelési mód"), ("Translate mode", "Fordító mód"), ("Use permanent password", "Állandó jelszó használata"), ("Use both passwords", "Mindkét jelszó használata"), @@ -323,15 +323,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Copied", "Másolva"), ("Exit Fullscreen", "Kilépés teljes képernyős módból"), ("Fullscreen", "Teljes képernyő"), - ("Mobile Actions", "mobil műveletek"), + ("Mobile Actions", "Mobil műveletek"), ("Select Monitor", "Válasszon képernyőt"), ("Control Actions", "Irányítási műveletek"), ("Display Settings", "Megjelenítési beállítások"), ("Ratio", "Arány"), ("Image Quality", "Képminőség"), ("Scroll Style", "Görgetési stílus"), - ("Show Toolbar", ""), - ("Hide Toolbar", ""), + ("Show Toolbar", "Eszköztár megjelenítése"), + ("Hide Toolbar", "Eszköztár eljertése"), ("Direct Connection", "Közvetlen kapcsolat"), ("Relay Connection", "Közvetett csatlakozás"), ("Secure Connection", "Biztonságos kapcsolat"), @@ -342,7 +342,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Security", "Biztonság"), ("Theme", "Téma"), ("Dark Theme", "Sötét téma"), - ("Light Theme", ""), + ("Light Theme", "Világos téma"), ("Dark", "Sötét"), ("Light", "Világos"), ("Follow System", "Rendszer téma követése"), @@ -359,12 +359,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Audio Input Device", "Audio bemeneti eszköz"), ("Use IP Whitelisting", "Engedélyezési lista használata"), ("Network", "Hálózat"), - ("Pin Toolbar", ""), - ("Unpin Toolbar", ""), + ("Pin Toolbar", "Eszköztár kitűzése"), + ("Unpin Toolbar", "Eszköztár kitűzésének feloldása"), ("Recording", "Felvétel"), ("Directory", "Könyvtár"), ("Automatically record incoming sessions", "A bejövő munkamenetek automatikus rögzítése"), - ("Automatically record outgoing sessions", ""), + ("Automatically record outgoing sessions", "A kimenő munkamenetek automatikus rögzítése"), ("Change", "Változtatás"), ("Start session recording", "Munkamenet rögzítés indítása"), ("Stop session recording", "Munkamenet rögzítés leállítása"), @@ -372,9 +372,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable LAN discovery", "Felfedezés enegedélyezése"), ("Deny LAN discovery", "Felfedezés tiltása"), ("Write a message", "Üzenet írása"), - ("Prompt", ""), - ("Please wait for confirmation of UAC...", ""), - ("elevated_foreground_window_tip", ""), + ("Prompt", "Kérés"), + ("Please wait for confirmation of UAC...", "Kérjük, várjon az UAC megerősítésére..."), + ("elevated_foreground_window_tip", "A távvezérelt számítógép jelenleg nyitott ablakához magasabb szintű jogok szükségesek. Ezért jelenleg nem lehetséges az egér és a billentyűzet használata. Kérje meg azt a felhasználót, akinek a számítógépét távolról vezérli, hogy minimalizálja az ablakot, vagy növelje a jogokat. A jövőbeni probléma elkerülése érdekében ajánlott a szoftvert a távvezérelt számítógépre telepíteni."), ("Disconnected", "Szétkapcsolva"), ("Other", "Egyéb"), ("Confirm before closing multiple tabs", "Biztos, hogy bezárja az összes lapot?"), @@ -389,269 +389,269 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("This PC", "Ez a számítógép"), ("or", "vagy"), ("Continue with", "Folytatás a következővel"), - ("Elevate", ""), - ("Zoom cursor", ""), - ("Accept sessions via password", ""), - ("Accept sessions via click", ""), - ("Accept sessions via both", ""), - ("Please wait for the remote side to accept your session request...", ""), + ("Elevate", "Hozzáférés engedélyezése"), + ("Zoom cursor", "Kurzor nagyítása"), + ("Accept sessions via password", "Munkamenetek elfogadása jelszóval"), + ("Accept sessions via click", "Munkamenetek elfogadása kattintással"), + ("Accept sessions via both", "Munkamenetek fogadása mindkettőn keresztül"), + ("Please wait for the remote side to accept your session request...", "Kérjük, várjon, amíg a távoli oldal elfogadja a munkamenet-kérelmét..."), ("One-time Password", "Egyszer használatos jelszó"), - ("Use one-time password", "Használj ideiglenes jelszót"), + ("Use one-time password", "Használjon ideiglenes jelszót"), ("One-time password length", "Egyszer használatos jelszó hossza"), - ("Request access to your device", ""), - ("Hide connection management window", ""), - ("hide_cm_tip", ""), - ("wayland_experiment_tip", ""), - ("Right click to select tabs", ""), - ("Skipped", ""), - ("Add to address book", ""), + ("Request access to your device", "Hozzáférés kérése az eszközéhez"), + ("Hide connection management window", "Kapcsolatkezelő ablak elrejtése"), + ("hide_cm_tip", "Ez csak akkor lehetséges, ha a hozzáférés állandó jelszóval történik."), + ("wayland_experiment_tip", "A Wayland-támogatás csak kísérleti jellegű. Kérjük, használja az X11-et, ha felügyelet nélküli hozzáférésre van szüksége."), + ("Right click to select tabs", "Jobb klikk a lapok kiválasztásához"), + ("Skipped", "Kihagyott"), + ("Add to address book", "Hozzáadás a címjegyzékhez"), ("Group", "Csoport"), ("Search", "Keresés"), - ("Closed manually by web console", ""), - ("Local keyboard type", ""), - ("Select local keyboard type", ""), - ("software_render_tip", ""), - ("Always use software rendering", ""), - ("config_input", ""), - ("config_microphone", ""), - ("request_elevation_tip", ""), - ("Wait", ""), - ("Elevation Error", ""), - ("Ask the remote user for authentication", ""), - ("Choose this if the remote account is administrator", ""), - ("Transmit the username and password of administrator", ""), - ("still_click_uac_tip", ""), - ("Request Elevation", ""), - ("wait_accept_uac_tip", ""), - ("Elevate successfully", ""), - ("uppercase", ""), - ("lowercase", ""), - ("digit", ""), - ("special character", ""), - ("length>=8", ""), - ("Weak", ""), - ("Medium", ""), - ("Strong", ""), - ("Switch Sides", ""), - ("Please confirm if you want to share your desktop?", ""), - ("Display", ""), + ("Closed manually by web console", "Kézzel zárva a webkonzolon keresztül"), + ("Local keyboard type", "Helyi billentyűzet típusa"), + ("Select local keyboard type", "Helyi billentyűzet típusának kiválasztása"), + ("software_render_tip", "Ha Nvidia grafikus kártyát használ Linux alatt, és a távoli ablak a kapcsolat létrehozása után azonnal bezáródik, akkor a Nouveau nyílt forráskódú illesztőprogramra való váltás és a szoftveres renderelés használata segíthet. A szoftvert újra kell indítani."), + ("Always use software rendering", "Mindig szoftveres renderelést használjon"), + ("config_input", "Ahhoz, hogy a távoli asztalt a billentyűzettel vezérelhesse, a RustDesknek meg kell adnia a \" Bemenet figyelése\" jogosultságot."), + ("config_microphone", "Ahhoz, hogy távolról beszélhessen, meg kell adnia a RustDesknek a \"Hangfelvétel\" jogosultságot."), + ("request_elevation_tip", "Akkor is kérhet megnövelt jogokat, ha valaki a partneroldalon van."), + ("Wait", "Várjon"), + ("Elevation Error", "Emeltszintű hozzáférési hiba"), + ("Ask the remote user for authentication", "Hitelesítés kérése a távoli felhasználótól"), + ("Choose this if the remote account is administrator", "Válassza ezt, ha a távoli fiók rendszergazda"), + ("Transmit the username and password of administrator", "Küldje el a rendszergazda felhasználónevét és jelszavát"), + ("still_click_uac_tip", "A távoli felhasználónak továbbra is a \"Igen\" gombra kell kattintania a RustDesk UAC ablakában. Kattintson!"), + ("Request Elevation", "Emelt szintű jogok igénylése"), + ("wait_accept_uac_tip", "Kérjük, várjon, amíg a távoli felhasználó elfogadja az UAC párbeszédet."), + ("Elevate successfully", "Emelt szintű jogok megadva"), + ("uppercase", "NAGYBETŰS"), + ("lowercase", "kisbetűs"), + ("digit", "szám"), + ("special character", "különleges karakter"), + ("length>=8", "hossz>=8"), + ("Weak", "Gyenge"), + ("Medium", "Közepes"), + ("Strong", "Erős"), + ("Switch Sides", "Oldalváltás"), + ("Please confirm if you want to share your desktop?", "Kérjük, erősítse meg, hogy meg akarja-e osztani az asztalát?"), + ("Display", "Képernyő"), ("Default View Style", "Alapértelmezett megjelenítés"), ("Default Scroll Style", "Alapértelmezett görgetés"), ("Default Image Quality", "Alapértelmezett képminőség"), ("Default Codec", "Alapértelmezett kódek"), - ("Bitrate", ""), - ("FPS", ""), - ("Auto", ""), - ("Other Default Options", ""), - ("Voice call", ""), - ("Text chat", ""), - ("Stop voice call", ""), - ("relay_hint_tip", ""), - ("Reconnect", ""), + ("Bitrate", "Bitsebesség"), + ("FPS", "FPS"), + ("Auto", "Automatikus"), + ("Other Default Options", "Egyéb alapértelmezett beállítások"), + ("Voice call", "Hanghívás"), + ("Text chat", "Szöveges csevegés"), + ("Stop voice call", "Hanghívás leállítása"), + ("relay_hint_tip", "Ha a közvetlen kapcsolat nem lehetséges, megpróbálhat kapcsolatot létesíteni egy relé-kiszolgálón keresztül.\nHa az első próbálkozáskor relé-kapcsolatot szeretne létrehozni, használhatja a \"/r\" utótagot. az azonosítóhoz vagy a \"Mindig relé-kiszolgálón keresztül csatlakozom\" opcióhoz a legutóbbi munkamenetek listájában, ha van ilyen."), + ("Reconnect", "Újracsatlakoztatás"), ("Codec", "Kódek"), ("Resolution", "Felbontás"), - ("No transfers in progress", ""), - ("Set one-time password length", ""), - ("RDP Settings", ""), - ("Sort by", ""), - ("New Connection", ""), - ("Restore", ""), - ("Minimize", ""), - ("Maximize", ""), - ("Your Device", ""), - ("empty_recent_tip", ""), - ("empty_favorite_tip", ""), - ("empty_lan_tip", ""), - ("empty_address_book_tip", ""), - ("eg: admin", ""), - ("Empty Username", ""), - ("Empty Password", ""), - ("Me", ""), - ("identical_file_tip", ""), - ("show_monitors_tip", ""), - ("View Mode", ""), - ("login_linux_tip", ""), - ("verify_rustdesk_password_tip", ""), - ("remember_account_tip", ""), - ("os_account_desk_tip", ""), - ("OS Account", ""), - ("another_user_login_title_tip", ""), - ("another_user_login_text_tip", ""), - ("xorg_not_found_title_tip", ""), - ("xorg_not_found_text_tip", ""), - ("no_desktop_title_tip", ""), - ("no_desktop_text_tip", ""), + ("No transfers in progress", "Nincs folyamatban átvitel"), + ("Set one-time password length", "Állítsa be az egyszeri jelszó hosszát"), + ("RDP Settings", "RDP beállítások"), + ("Sort by", "Rendezés"), + ("New Connection", "Új kapcsolat"), + ("Restore", "Visszaállítás"), + ("Minimize", "Minimalizálás"), + ("Maximize", "Maximalizálás"), + ("Your Device", "Az Ön eszköze"), + ("empty_recent_tip", "Hoppá, nincsenek aktuális munkamenetek!\nIdeje ütemezni egy újat."), + ("empty_favorite_tip", "Még nincs kedvenc távoli állomás?\nHagyd, hogy találjunk valakit, akivel kapcsolatba tudunk lépni, és add hozzá a kedvenceidhez!"), + ("empty_lan_tip", "Ó, nem, úgy tűnik, még nem fedeztünk fel egy távoli helyszínt."), + ("empty_address_book_tip", "Ó, kedvesem, úgy tűnik, hogy jelenleg nincsenek távoli állomások a címjegyzékében."), + ("eg: admin", "pl: admin"), + ("Empty Username", "Üres felhasználónév"), + ("Empty Password", "Üres jelszó"), + ("Me", "Én"), + ("identical_file_tip", "Ez a fájl megegyezik a távoli állomás fájljával."), + ("show_monitors_tip", "Képernyők megjelenítése az eszköztáron"), + ("View Mode", "Nézet mód"), + ("login_linux_tip", "Az X-asztal munkamenet megnyitásához be kell jelentkeznie egy távoli Linux-fiókba."), + ("verify_rustdesk_password_tip", "RustDesk jelszó megerősítése"), + ("remember_account_tip", "Emlékezzen erre a fiókra"), + ("os_account_desk_tip", "Ezzel a fiókkal bejelentkezhet a távoli operációs rendszerbe, és aktiválhatja az asztali munkamenetet fej nélküli módban."), + ("OS Account", "OS fiók"), + ("another_user_login_title_tip", "Egy másik felhasználó már bejelentkezett."), + ("another_user_login_text_tip", "Különálló"), + ("xorg_not_found_title_tip", "Xorg nem található."), + ("xorg_not_found_text_tip", "Kérjük, telepítse az Xorg-ot."), + ("no_desktop_title_tip", "Nem áll rendelkezésre asztali környezet."), + ("no_desktop_text_tip", "Kérjük, telepítse a GNOME asztalt."), ("No need to elevate", ""), - ("System Sound", ""), - ("Default", ""), - ("New RDP", ""), - ("Fingerprint", ""), - ("Copy Fingerprint", ""), - ("no fingerprints", ""), - ("Select a peer", ""), - ("Select peers", ""), - ("Plugins", ""), - ("Uninstall", ""), - ("Update", ""), - ("Enable", ""), - ("Disable", ""), - ("Options", ""), - ("resolution_original_tip", ""), - ("resolution_fit_local_tip", ""), - ("resolution_custom_tip", ""), - ("Collapse toolbar", ""), - ("Accept and Elevate", ""), - ("accept_and_elevate_btn_tooltip", ""), - ("clipboard_wait_response_timeout_tip", ""), - ("Incoming connection", ""), - ("Outgoing connection", ""), - ("Exit", ""), - ("Open", ""), - ("logout_tip", ""), + ("System Sound", "A jogok növelése nem szükséges"), + ("Default", "Alapértelmezett"), + ("New RDP", "Új RDP"), + ("Fingerprint", "Ujjlenyomat"), + ("Copy Fingerprint", "Ujjlenyomat másolása"), + ("no fingerprints", "nincsenek ujjlenyomatok"), + ("Select a peer", "Egy távoli állomás kiválasztása"), + ("Select peers", "Távoli állomások kiválasztása"), + ("Plugins", "Beépülő modulok"), + ("Uninstall", "Eltávolítás"), + ("Update", "Frissítés"), + ("Enable", "Engedélyezés"), + ("Disable", "Letiltás"), + ("Options", "Beállítások"), + ("resolution_original_tip", "Eredeti felbontás"), + ("resolution_fit_local_tip", "Helyi felbontás beállítása"), + ("resolution_custom_tip", "Testreszabható felbontás"), + ("Collapse toolbar", "Eszköztár összecsukása"), + ("Accept and Elevate", "Elfogadás és magasabb szintű jogrosultságra emelés"), + ("accept_and_elevate_btn_tooltip", "Fogadja el a kapcsolatot, és növelje az UAC-engedélyeket."), + ("clipboard_wait_response_timeout_tip", "Időtúllépés, amíg a másolat válaszára vár."), + ("Incoming connection", "Bejövő kapcsolat"), + ("Outgoing connection", "Kimenő kapcsolat"), + ("Exit", "Kilépés"), + ("Open", "Megnyitás"), + ("logout_tip", "Biztosan le szeretne iratkozni?"), ("Service", "Szolgáltatás"), ("Start", "Indítás"), ("Stop", "Leállítás"), - ("exceed_max_devices", ""), - ("Sync with recent sessions", ""), - ("Sort tags", ""), - ("Open connection in new tab", ""), - ("Move tab to new window", ""), - ("Can not be empty", ""), - ("Already exists", ""), - ("Change Password", ""), - ("Refresh Password", ""), - ("ID", ""), - ("Grid View", ""), - ("List View", ""), - ("Select", ""), - ("Toggle Tags", ""), - ("pull_ab_failed_tip", ""), - ("push_ab_failed_tip", ""), - ("synced_peer_readded_tip", ""), - ("Change Color", ""), - ("Primary Color", ""), - ("HSV Color", ""), - ("Installation Successful!", ""), - ("Installation failed!", ""), - ("Reverse mouse wheel", ""), - ("{} sessions", ""), - ("scam_title", ""), - ("scam_text1", ""), - ("scam_text2", ""), - ("Don't show again", ""), - ("I Agree", ""), - ("Decline", ""), - ("Timeout in minutes", ""), - ("auto_disconnect_option_tip", ""), - ("Connection failed due to inactivity", ""), - ("Check for software update on startup", ""), - ("upgrade_rustdesk_server_pro_to_{}_tip", ""), - ("pull_group_failed_tip", ""), - ("Filter by intersection", ""), - ("Remove wallpaper during incoming sessions", ""), - ("Test", ""), - ("display_is_plugged_out_msg", ""), - ("No displays", ""), - ("Open in new window", ""), - ("Show displays as individual windows", ""), - ("Use all my displays for the remote session", ""), - ("selinux_tip", ""), - ("Change view", ""), - ("Big tiles", ""), - ("Small tiles", ""), - ("List", ""), - ("Virtual display", ""), - ("Plug out all", ""), - ("True color (4:4:4)", ""), - ("Enable blocking user input", ""), - ("id_input_tip", ""), - ("privacy_mode_impl_mag_tip", ""), - ("privacy_mode_impl_virtual_display_tip", ""), - ("Enter privacy mode", ""), - ("Exit privacy mode", ""), - ("idd_not_support_under_win10_2004_tip", ""), - ("input_source_1_tip", ""), - ("input_source_2_tip", ""), - ("Swap control-command key", ""), - ("swap-left-right-mouse", ""), - ("2FA code", ""), - ("More", ""), - ("enable-2fa-title", ""), - ("enable-2fa-desc", ""), - ("wrong-2fa-code", ""), - ("enter-2fa-title", ""), - ("Email verification code must be 6 characters.", ""), - ("2FA code must be 6 digits.", ""), - ("Multiple Windows sessions found", ""), - ("Please select the session you want to connect to", ""), - ("powered_by_me", ""), - ("outgoing_only_desk_tip", ""), - ("preset_password_warning", ""), - ("Security Alert", ""), - ("My address book", ""), - ("Personal", ""), - ("Owner", ""), - ("Set shared password", ""), - ("Exist in", ""), - ("Read-only", ""), - ("Read/Write", ""), - ("Full Control", ""), - ("share_warning_tip", ""), - ("Everyone", ""), - ("ab_web_console_tip", ""), - ("allow-only-conn-window-open-tip", ""), - ("no_need_privacy_mode_no_physical_displays_tip", ""), - ("Follow remote cursor", ""), - ("Follow remote window focus", ""), - ("default_proxy_tip", ""), - ("no_audio_input_device_tip", ""), - ("Incoming", ""), - ("Outgoing", ""), - ("Clear Wayland screen selection", ""), - ("clear_Wayland_screen_selection_tip", ""), - ("confirm_clear_Wayland_screen_selection_tip", ""), - ("android_new_voice_call_tip", ""), - ("texture_render_tip", ""), - ("Use texture rendering", ""), - ("Floating window", ""), - ("floating_window_tip", ""), - ("Keep screen on", ""), - ("Never", ""), - ("During controlled", ""), - ("During service is on", ""), - ("Capture screen using DirectX", ""), - ("Back", ""), - ("Apps", ""), - ("Volume up", ""), - ("Volume down", ""), - ("Power", ""), - ("Telegram bot", ""), - ("enable-bot-tip", ""), - ("enable-bot-desc", ""), - ("cancel-2fa-confirm-tip", ""), - ("cancel-bot-confirm-tip", ""), - ("About RustDesk", ""), - ("Send clipboard keystrokes", ""), - ("network_error_tip", ""), - ("Unlock with PIN", ""), - ("Requires at least {} characters", ""), - ("Wrong PIN", ""), - ("Set PIN", ""), - ("Enable trusted devices", ""), - ("Manage trusted devices", ""), - ("Platform", ""), - ("Days remaining", ""), - ("enable-trusted-devices-tip", ""), - ("Parent directory", ""), - ("Resume", ""), - ("Invalid file name", ""), - ("one-way-file-transfer-tip", ""), - ("Authentication Required", ""), - ("Authenticate", ""), - ("web_id_input_tip", ""), - ("Download", ""), - ("Upload folder", ""), - ("Upload files", ""), - ("Clipboard is synchronized", ""), + ("exceed_max_devices", "Elérte a felügyelt eszközök maximális számát."), + ("Sync with recent sessions", "Szinkronizálás a legutóbbi munkamenetekkel"), + ("Sort tags", "Címkék rendezése"), + ("Open connection in new tab", "Kapcsolat megnyitása új lapon"), + ("Move tab to new window", "Lap áthelyezése új ablakba"), + ("Can not be empty", "Nem lehet üres"), + ("Already exists", "Már létezik"), + ("Change Password", "Jelszó módosítása"), + ("Refresh Password", "Jelszó frissítése"), + ("ID", "ID"), + ("Grid View", "Mozaik nézet"), + ("List View", "Lista nézet"), + ("Select", "Kiválasztás"), + ("Toggle Tags", "Címke kapcsoló"), + ("pull_ab_failed_tip", "A címjegyzék frissítése nem sikerült"), + ("push_ab_failed_tip", "A címjegyzék szinkronizálása a szerverrel nem sikerült"), + ("synced_peer_readded_tip", "A legutóbbi munkamenetekben jelen lévő eszközök ismét felkerülnek a címjegyzékbe."), + ("Change Color", "Szín módosítása"), + ("Primary Color", "Elsődleges szín"), + ("HSV Color", "HSV szín"), + ("Installation Successful!", "Sikeres telepítés!"), + ("Installation failed!", "A telepítés nem sikerült!"), + ("Reverse mouse wheel", "Fordított egérgörgő"), + ("{} sessions", "{} munkamenet"), + ("scam_title", "Lehet, hogy átverték!"), + ("scam_text1", "Ha olyan valakivel beszél telefonon, akit NEM ISMER, akiben NEM BÍZIK MEG, és aki arra kéri, hogy használja a RustDesket és indítsa el a szolgáltatást, ne folytassa, és azonnal tegye le a telefont."), + ("scam_text2", "Valószínűleg egy csaló próbálja ellopni a pénzét vagy más személyes adatait."), + ("Don't show again", "Ne mutasd újra"), + ("I Agree", "Elfogadom"), + ("Decline", "Elutasítom"), + ("Timeout in minutes", "Időtúllépés percekben"), + ("auto_disconnect_option_tip", "A bejövő munkamenetek automatikus bezárása, ha a felhasználó inaktív"), + ("Connection failed due to inactivity", "A kapcsolat inaktivitás miatt megszakadt"), + ("Check for software update on startup", "Szoftverfrissítés keresése indításkor"), + ("upgrade_rustdesk_server_pro_to_{}_tip", "Kérjük, frissítse a RustDesk Server Pro-t a(z) {} vagy újabb verzióra!"), + ("pull_group_failed_tip", "A csoport frissítése nem sikerült"), + ("Filter by intersection", "Szűrés metszéspontok szerint"), + ("Remove wallpaper during incoming sessions", "Távolítsa el a háttérképet a bejövő munkamenetek során"), + ("Test", "Teszt"), + ("display_is_plugged_out_msg", "A képernyő nincs csatlakoztatva, váltson az első képernyőre."), + ("No displays", "Nincsenek kijelzők"), + ("Open in new window", "Megnyitás új ablakban"), + ("Show displays as individual windows", "Kijelzők megjelenítése egyedi ablakokként"), + ("Use all my displays for the remote session", "Az összes kijelzőm használata a távoli munkamenethez"), + ("selinux_tip", "A SELinux engedélyezve van az eszközén, ami azt okozhatja, hogy a RustDesk nem fut megfelelően, mint ellenőrzött webhely."), + ("Change view", "Nézet módosítása"), + ("Big tiles", "Nagy csempék"), + ("Small tiles", "Kis csempék"), + ("List", "Lista"), + ("Virtual display", "Virtuális kijelző"), + ("Plug out all", "Kapcsolja ki az összeset"), + ("True color (4:4:4)", "Valódi szín (4:4:4)"), + ("Enable blocking user input", "Engedélyezze a felhasználói bevitel blokkolását"), + ("id_input_tip", "Megadhat egy azonosítót, egy közvetlen IP címet vagy egy tartományt egy porttal (:).\nHa egy másik szerveren lévő eszközhöz szeretne hozzáférni, adja meg a szerver címét (@?key=), például\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nHa egy nyilvános szerveren lévő eszközhöz szeretne hozzáférni, adja meg a \"@public\" lehetőséget. in. A kulcsra nincs szükség nyilvános szerverek esetén.\n\nHa az első kapcsolathoz relé-kapcsolatot akar kényszeríteni, adjon hozzá \"/r\" az azonosító végén, például \"9123456234/r\"."), + ("privacy_mode_impl_mag_tip", "1. mód"), + ("privacy_mode_impl_virtual_display_tip", "2. mód"), + ("Enter privacy mode", "Lépjen be az adatvédelmi módba"), + ("Exit privacy mode", "Lépjen ki az adatvédelmi módból"), + ("idd_not_support_under_win10_2004_tip", "A közvetett grafikus illesztőprogram nem támogatott. Windows 10, 2004-es vagy újabb verzió szükséges."), + ("input_source_1_tip", "1. bemeneti forrás"), + ("input_source_2_tip", "2. bemeneti forrás"), + ("Swap control-command key", "Vezérlő- és parancsgombok cseréje"), + ("swap-left-right-mouse", "Bal és jobb egérgomb felcserélése"), + ("2FA code", "2FA kód"), + ("More", "További"), + ("enable-2fa-title", "Kétfaktoros hitelesítés aktiválása"), + ("enable-2fa-desc", "Kérjük, most állítsa be a hitelesítőt. Használhat egy hitelesítési alkalmazást, például az Authy, a Microsoft vagy a Google Authenticator alkalmazást a telefonján vagy az asztali számítógépén.\n\nScannelje be a QR-kódot az alkalmazással, és adja meg az alkalmazás által megjelenített kódot a kétfaktoros hitelesítés aktiválásához."), + ("wrong-2fa-code", "A kód nem ellenőrizhető. Ellenőrizze, hogy a kód és a helyi idő beállításai helyesek-e."), + ("enter-2fa-title", "Kétfaktoros hitelesítés"), + ("Email verification code must be 6 characters.", "Az e-mail ellenőrző kódnak 6 karakterből kell állnia."), + ("2FA code must be 6 digits.", "A 2FA-kódnak 6 számjegyűnek kell lennie."), + ("Multiple Windows sessions found", "Több Windows munkamenet található"), + ("Please select the session you want to connect to", "Kérjük, válassza ki a munkamenetet, amelyhez csatlakozni szeretne"), + ("powered_by_me", "Üzemeltető: RustDesk"), + ("outgoing_only_desk_tip", "Ez a RustDesk testreszabott kimenete.\nMás eszközökhöz csatlakozhat, de más eszközök nem csatlakozhatnak az Ön eszközéhez."), + ("preset_password_warning", "Ez egy testreszabott kimenet a RustDeskből egy előre beállított jelszóval. Bárki, aki ismeri ezt a jelszót, teljes irányítást szerezhet a készülék felett. Ha nem kívánja ezt megtenni, kérjük, azonnal távolítsa el ezt a szoftvert."), + ("Security Alert", "Biztonsági riasztás"), + ("My address book", "Címjegyzékem"), + ("Personal", "Személyes"), + ("Owner", "Tulajdonos"), + ("Set shared password", "Megosztott jelszó beállítása"), + ("Exist in", "Létezik"), + ("Read-only", "Csak olvasható"), + ("Read/Write", "Olvasás/Írás"), + ("Full Control", "Teljes ellenőrzés"), + ("share_warning_tip", "A fenti mezők megosztottak és mások számára is láthatóak."), + ("Everyone", "Mindenki"), + ("ab_web_console_tip", "További információk a webes konzolról"), + ("allow-only-conn-window-open-tip", "Csak akkor engedélyezze a csatlakozást, ha a RustDesk ablak nyitva van."), + ("no_need_privacy_mode_no_physical_displays_tip", "Nincsenek fizikai képernyők; Nincs szükség az adatvédelmi üzemmód használatára."), + ("Follow remote cursor", "Kövesse a távoli kurzort"), + ("Follow remote window focus", "Kövesse a távoli ablak fókuszt"), + ("default_proxy_tip", "A szabványos protokoll és port SOCKS5 és 1080"), + ("no_audio_input_device_tip", "Nem található hangbemeneti eszköz."), + ("Incoming", "Bejövő"), + ("Outgoing", "Kimenő"), + ("Clear Wayland screen selection", "Wayland képernyő kiválasztásának törlése"), + ("clear_Wayland_screen_selection_tip", "A képernyőválasztás törlése után újra kiválaszthatja a megosztandó képernyőt."), + ("confirm_clear_Wayland_screen_selection_tip", "Biztos, hogy törölni szeretné a Wayland képernyő kiválasztását?"), + ("android_new_voice_call_tip", "Új hanghívás-kérés érkezett. Ha elfogadja a megkeresést, a hang átvált hangkommunikációra."), + ("texture_render_tip", "Használja a textúra renderelést a képek simábbá tételéhez. Ezt az opciót kikapcsolhatja, ha renderelési problémái vannak."), + ("Use texture rendering", "Textúra renderelés használata"), + ("Floating window", "Lebegő ablak"), + ("floating_window_tip", "Segít, ha a RustDesk a háttérben fut."), + ("Keep screen on", "Tartsa a képernyőt bekapcsolva"), + ("Never", "Soha"), + ("During controlled", "Amikor ellenőrzött"), + ("During service is on", "Amikor a szolgáltatás fut"), + ("Capture screen using DirectX", "Képernyő rögzítése DirectX használatával"), + ("Back", "Vissza"), + ("Apps", "Alkalmazások"), + ("Volume up", "Hangerő fel"), + ("Volume down", "Hangerő le"), + ("Power", "Teljesítmény"), + ("Telegram bot", "Telegram bot"), + ("enable-bot-tip", "Ha aktiválja ezt a funkciót, akkor a 2FA-kódot a botjától kaphatja meg. Kapcsolati értesítésként is használható."), + ("enable-bot-desc", "1. Nyisson csevegést @BotFather.\n2. Küldje el a \"/newbot\" parancsot. Miután ezt a lépést elvégezte, kap egy tokent.\n3. Indítson csevegést az újonnan létrehozott botjával. Küldjön egy olyan üzenetet, amely egy perjelrel kezdődik (\"/\"), pl. B. \"/hello\" az aktiváláshoz.\n"), + ("cancel-2fa-confirm-tip", "Biztos, hogy le akarja mondani a 2FA-t?"), + ("cancel-bot-confirm-tip", "Biztos, hogy le akarod mondani a Telegram botot?"), + ("About RustDesk", "A RustDeskről"), + ("Send clipboard keystrokes", "Vágólap billentyűleütések küldése"), + ("network_error_tip", "Kérjük, ellenőrizze a hálózati kapcsolatot, majd próbálja meg újra."), + ("Unlock with PIN", "Feloldás PIN-kóddal"), + ("Requires at least {} characters", "Legalább {} karakter szükséges"), + ("Wrong PIN", "Hibás PIN"), + ("Set PIN", "PIN-kód beállítása"), + ("Enable trusted devices", "Megbízható eszközök engedélyezése"), + ("Manage trusted devices", "Megbízható eszközök kezelése"), + ("Platform", "Platform"), + ("Days remaining", "Hátralévő napok"), + ("enable-trusted-devices-tip", "A 2FA-ellenőrzés kihagyása megbízható eszközökön"), + ("Parent directory", "Szülőkönyvtár"), + ("Resume", "Folytatás"), + ("Invalid file name", "Érvénytelen fájlnév"), + ("one-way-file-transfer-tip", "Az egyirányú fájlátvitel engedélyezve van a vezérelt oldalon."), + ("Authentication Required", "Hitelesítés szükséges"), + ("Authenticate", "Hitelesítés"), + ("web_id_input_tip", "Azonos szerveren lévő azonosítót adhat meg, a közvetlen IP elérés nem támogatott a webkliensben.\nHa egy másik szerveren lévő eszközhöz szeretne hozzáférni, kérjük, adja meg a szerver címét (@?key=), például\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nHa egy nyilvános szerveren lévő eszközhöz szeretne hozzáférni, kérjük, adja meg a \"@public\" betűt. in. A kulcsra nincs szükség a nyilvános szerverek esetében."), + ("Download", "Letöltés"), + ("Upload folder", "Mappa feltöltése"), + ("Upload files", "Fájlok feltöltése"), + ("Clipboard is synchronized", "A vágólap szinkronizálva van"), ].iter().cloned().collect(); } From eba19e67ffc5fe09c5e06a2e56197fd686d35029 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Tue, 29 Oct 2024 14:19:16 +0800 Subject: [PATCH 332/541] fix: mobile input (#9769) 1. Map mode. Check if the KeyEvent's usbHidUsage is correct. 2. Korean input, use listener to handle composing state. Signed-off-by: fufesou --- flutter/lib/mobile/pages/remote_page.dart | 26 ++++++++++++++--------- flutter/lib/models/input_model.dart | 20 ++++++++++++++++- 2 files changed, 35 insertions(+), 11 deletions(-) diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index 40890f228e6b..a8446de20225 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -57,9 +57,7 @@ class _RemotePageState extends State { final TextEditingController _textController = TextEditingController(text: initText); - // This timer is used to check the composing status of the soft keyboard. - // It is used for Android, Korean(and other similar) input method. - Timer? _composingTimer; + bool _lastComposingChangeValid = false; _RemotePageState(String id) { initSharedStates(id); @@ -99,6 +97,9 @@ class _RemotePageState extends State { showToast(translate('Automatically record outgoing sessions')); } }); + if (isAndroid) { + _textController.addListener(textAndroidListener); + } } @override @@ -114,7 +115,6 @@ class _RemotePageState extends State { _physicalFocusNode.dispose(); await gFFI.close(); _timer?.cancel(); - _composingTimer?.cancel(); gFFI.dialogManager.dismissAll(); await SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: SystemUiOverlay.values); @@ -127,6 +127,16 @@ class _RemotePageState extends State { // The inner logic of `on_voice_call_closed` will check if the voice call is active. // Only one client is considered here for now. gFFI.chatModel.onVoiceCallClosed("End connetion"); + if (isAndroid) { + _textController.removeListener(textAndroidListener); + } + } + + // This listener is used to handle the composing region changes for Android soft keyboard input. + void textAndroidListener() { + if (_lastComposingChangeValid) { + _handleNonIOSSoftKeyboardInput(_textController.text); + } } // to-do: It should be better to use transparent color instead of the bgColor. @@ -150,7 +160,6 @@ class _RemotePageState extends State { gFFI.ffiModel.pi.version.isNotEmpty) { gFFI.invokeMethod("enable_soft_keyboard", false); } - _composingTimer?.cancel(); } else { _timer?.cancel(); _timer = Timer(kMobileDelaySoftKeyboardFocus, () { @@ -214,11 +223,8 @@ class _RemotePageState extends State { } void _handleNonIOSSoftKeyboardInput(String newValue) { - _composingTimer?.cancel(); - if (_textController.value.isComposingRangeValid) { - _composingTimer = Timer(Duration(milliseconds: 25), () { - _handleNonIOSSoftKeyboardInput(_textController.value.text); - }); + _lastComposingChangeValid = _textController.value.isComposingRangeValid; + if (_lastComposingChangeValid) { return; } var oldValue = _value; diff --git a/flutter/lib/models/input_model.dart b/flutter/lib/models/input_model.dart index c7e1e6131c3d..2b00098caafc 100644 --- a/flutter/lib/models/input_model.dart +++ b/flutter/lib/models/input_model.dart @@ -544,7 +544,25 @@ class InputModel { handleKeyDownEventModifiers(e); } - if (isMobile || (isDesktop || isWebDesktop) && keyboardMode == kKeyMapMode) { + // The physicalKey.usbHidUsage may be not correct for soft keyboard on Android. + // iOS does not have this issue. + // 1. Open the soft keyboard on Android + // 2. Switch to input method like zh/ko/ja + // 3. Click Backspace and Enter on the soft keyboard or physical keyboard + // 4. The physicalKey.usbHidUsage is not correct. + // PhysicalKeyboardKey#8ac83(usbHidUsage: "0x1100000042", debugName: "Key with ID 0x1100000042") + // LogicalKeyboardKey#2604c(keyId: "0x10000000d", keyLabel: "Enter", debugName: "Enter") + // + // The correct PhysicalKeyboardKey should be + // PhysicalKeyboardKey#e14a9(usbHidUsage: "0x00070028", debugName: "Enter") + // https://github.com/flutter/flutter/issues/157771 + final isKeyMatch = + isIOS || isAndroid && e.logicalKey.debugName == e.physicalKey.debugName; + final isMobileAndPeerNotAndroid = + isMobile && peerPlatform != kPeerPlatformAndroid; + final isDesktopAndMapMode = + isDesktop || isWebDesktop && keyboardMode == kKeyMapMode; + if (isKeyMatch && (isMobileAndPeerNotAndroid || isDesktopAndMapMode)) { // FIXME: e.character is wrong for dead keys, eg: ^ in de newKeyboardMode( e.character ?? '', From 1c9b456456d866b73003a58d97c28c7c788eee76 Mon Sep 17 00:00:00 2001 From: RustDesk <71636191+rustdesk@users.noreply.github.com> Date: Tue, 29 Oct 2024 15:33:12 +0800 Subject: [PATCH 333/541] Update bug_report.yaml (#9771) --- .github/ISSUE_TEMPLATE/bug_report.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index d8781bb0c6be..0615604ed529 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -27,7 +27,7 @@ body: id: os attributes: label: Operating system(s) on local side and remote side - description: What operating system(s) do you see this bug on? local side -> remote side. + description: What operating system(s) do you see this bug on? local (controlling) side -> remote (controlled) side. placeholder: | Windows 10 -> osx validations: From a289eae07c6c99480f0d758660150fc35e1523fb Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Tue, 29 Oct 2024 19:57:29 +0800 Subject: [PATCH 334/541] fix: mobile -> mobile, long press (#9775) Signed-off-by: fufesou --- flutter/lib/common/widgets/remote_input.dart | 4 +++- flutter/lib/models/model.dart | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/flutter/lib/common/widgets/remote_input.dart b/flutter/lib/common/widgets/remote_input.dart index a4d3caf2990a..4a82dfae3262 100644 --- a/flutter/lib/common/widgets/remote_input.dart +++ b/flutter/lib/common/widgets/remote_input.dart @@ -185,7 +185,9 @@ class _RawTouchGestureDetectorRegionState ffi.cursorModel .move(_cacheLongPressPosition.dx, _cacheLongPressPosition.dy); } - inputModel.tap(MouseButtons.right); + if (!ffi.ffiModel.isPeerMobile) { + inputModel.tap(MouseButtons.right); + } } onDoubleFinerTapDown(TapDownDetails d) { diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index ecbfd6fa4398..bd844c3ca306 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -142,6 +142,7 @@ class FfiModel with ChangeNotifier { bool get touchMode => _touchMode; bool get isPeerAndroid => _pi.platform == kPeerPlatformAndroid; + bool get isPeerMobile => isPeerAndroid; bool get viewOnly => _viewOnly; From 415d2c5c60a309f4b869452366acfbf19e75fca7 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Tue, 29 Oct 2024 22:52:46 +0800 Subject: [PATCH 335/541] OPTION_VERIFICATION_METHOD --- libs/hbb_common/src/config.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/libs/hbb_common/src/config.rs b/libs/hbb_common/src/config.rs index 0cb370cd812e..73b330761a91 100644 --- a/libs/hbb_common/src/config.rs +++ b/libs/hbb_common/src/config.rs @@ -2225,6 +2225,7 @@ pub mod keys { pub const OPTION_ALLOW_LINUX_HEADLESS: &str = "allow-linux-headless"; pub const OPTION_ENABLE_HWCODEC: &str = "enable-hwcodec"; pub const OPTION_APPROVE_MODE: &str = "approve-mode"; + pub const OPTION_VERIFICATION_METHOD: &str = "verification-method"; pub const OPTION_CUSTOM_RENDEZVOUS_SERVER: &str = "custom-rendezvous-server"; pub const OPTION_API_SERVER: &str = "api-server"; pub const OPTION_KEY: &str = "key"; @@ -2370,6 +2371,7 @@ pub mod keys { OPTION_ALLOW_LINUX_HEADLESS, OPTION_ENABLE_HWCODEC, OPTION_APPROVE_MODE, + OPTION_VERIFICATION_METHOD, OPTION_PROXY_URL, OPTION_PROXY_USERNAME, OPTION_PROXY_PASSWORD, From ce7867c1c0ab534afdf4082474a5369d3253b145 Mon Sep 17 00:00:00 2001 From: 21pages Date: Wed, 30 Oct 2024 11:29:39 +0800 Subject: [PATCH 336/541] fix wrong display of custom clients when approval mode is not set (#9779) when approve-mode is not set, the approve mode option shows as password, it's `both` approve mode in rust, so only ui is wrong. Signed-off-by: 21pages --- flutter/lib/desktop/pages/desktop_setting_page.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 15cf2173b5e9..69100470f0ea 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -1029,7 +1029,9 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { translate('Accept sessions via both'), ]; var modeInitialKey = model.approveMode; - if (!modeKeys.contains(modeInitialKey)) modeInitialKey = ''; + if (!modeKeys.contains(modeInitialKey)) { + modeInitialKey = defaultOptionApproveMode; + } final usePassword = model.approveMode != 'click'; final isApproveModeFixed = isOptionFixed(kOptionApproveMode); From 0f5f9f65245e40b6c0ebb434308be0fe4b2b129b Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Wed, 30 Oct 2024 12:17:13 +0800 Subject: [PATCH 337/541] revert: #9769 (#9780) Not sure TextEditingController.addListener() can handle all composing changes. https://github.com/rustdesk/rustdesk/issues/7727#issuecomment-2445721499 Signed-off-by: fufesou --- flutter/lib/mobile/pages/remote_page.dart | 26 +++++++++-------------- 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index a8446de20225..40890f228e6b 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -57,7 +57,9 @@ class _RemotePageState extends State { final TextEditingController _textController = TextEditingController(text: initText); - bool _lastComposingChangeValid = false; + // This timer is used to check the composing status of the soft keyboard. + // It is used for Android, Korean(and other similar) input method. + Timer? _composingTimer; _RemotePageState(String id) { initSharedStates(id); @@ -97,9 +99,6 @@ class _RemotePageState extends State { showToast(translate('Automatically record outgoing sessions')); } }); - if (isAndroid) { - _textController.addListener(textAndroidListener); - } } @override @@ -115,6 +114,7 @@ class _RemotePageState extends State { _physicalFocusNode.dispose(); await gFFI.close(); _timer?.cancel(); + _composingTimer?.cancel(); gFFI.dialogManager.dismissAll(); await SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: SystemUiOverlay.values); @@ -127,16 +127,6 @@ class _RemotePageState extends State { // The inner logic of `on_voice_call_closed` will check if the voice call is active. // Only one client is considered here for now. gFFI.chatModel.onVoiceCallClosed("End connetion"); - if (isAndroid) { - _textController.removeListener(textAndroidListener); - } - } - - // This listener is used to handle the composing region changes for Android soft keyboard input. - void textAndroidListener() { - if (_lastComposingChangeValid) { - _handleNonIOSSoftKeyboardInput(_textController.text); - } } // to-do: It should be better to use transparent color instead of the bgColor. @@ -160,6 +150,7 @@ class _RemotePageState extends State { gFFI.ffiModel.pi.version.isNotEmpty) { gFFI.invokeMethod("enable_soft_keyboard", false); } + _composingTimer?.cancel(); } else { _timer?.cancel(); _timer = Timer(kMobileDelaySoftKeyboardFocus, () { @@ -223,8 +214,11 @@ class _RemotePageState extends State { } void _handleNonIOSSoftKeyboardInput(String newValue) { - _lastComposingChangeValid = _textController.value.isComposingRangeValid; - if (_lastComposingChangeValid) { + _composingTimer?.cancel(); + if (_textController.value.isComposingRangeValid) { + _composingTimer = Timer(Duration(milliseconds: 25), () { + _handleNonIOSSoftKeyboardInput(_textController.value.text); + }); return; } var oldValue = _value; From e6c5064ce53baee2fafcdc355653a712a66a29d7 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Wed, 30 Oct 2024 13:05:25 +0800 Subject: [PATCH 338/541] fix: android input, Backspace and Enter (#9782) Signed-off-by: fufesou --- flutter/lib/models/input_model.dart | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/flutter/lib/models/input_model.dart b/flutter/lib/models/input_model.dart index 2b00098caafc..1b2a9736ee2a 100644 --- a/flutter/lib/models/input_model.dart +++ b/flutter/lib/models/input_model.dart @@ -556,13 +556,14 @@ class InputModel { // The correct PhysicalKeyboardKey should be // PhysicalKeyboardKey#e14a9(usbHidUsage: "0x00070028", debugName: "Enter") // https://github.com/flutter/flutter/issues/157771 - final isKeyMatch = - isIOS || isAndroid && e.logicalKey.debugName == e.physicalKey.debugName; + // We cannot use the debugName to determine the key is correct or not, because it's null in release mode. + // to-do: `isLegacyModeKeys` is not the best workaround, we need to find a better way to fix this issue. + final isLegacyModeKeys = ['Backspace', 'Enter'].contains(e.logicalKey.keyLabel); final isMobileAndPeerNotAndroid = isMobile && peerPlatform != kPeerPlatformAndroid; final isDesktopAndMapMode = isDesktop || isWebDesktop && keyboardMode == kKeyMapMode; - if (isKeyMatch && (isMobileAndPeerNotAndroid || isDesktopAndMapMode)) { + if (!isLegacyModeKeys && (isMobileAndPeerNotAndroid || isDesktopAndMapMode)) { // FIXME: e.character is wrong for dead keys, eg: ^ in de newKeyboardMode( e.character ?? '', From 711ed28846221afee485e8826e7d738990f83952 Mon Sep 17 00:00:00 2001 From: RustDesk <71636191+rustdesk@users.noreply.github.com> Date: Wed, 30 Oct 2024 13:12:18 +0800 Subject: [PATCH 339/541] Revert "revert: #9769 (#9780)" (#9783) This reverts commit 0f5f9f65245e40b6c0ebb434308be0fe4b2b129b. --- flutter/lib/mobile/pages/remote_page.dart | 26 ++++++++++++++--------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index 40890f228e6b..a8446de20225 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -57,9 +57,7 @@ class _RemotePageState extends State { final TextEditingController _textController = TextEditingController(text: initText); - // This timer is used to check the composing status of the soft keyboard. - // It is used for Android, Korean(and other similar) input method. - Timer? _composingTimer; + bool _lastComposingChangeValid = false; _RemotePageState(String id) { initSharedStates(id); @@ -99,6 +97,9 @@ class _RemotePageState extends State { showToast(translate('Automatically record outgoing sessions')); } }); + if (isAndroid) { + _textController.addListener(textAndroidListener); + } } @override @@ -114,7 +115,6 @@ class _RemotePageState extends State { _physicalFocusNode.dispose(); await gFFI.close(); _timer?.cancel(); - _composingTimer?.cancel(); gFFI.dialogManager.dismissAll(); await SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: SystemUiOverlay.values); @@ -127,6 +127,16 @@ class _RemotePageState extends State { // The inner logic of `on_voice_call_closed` will check if the voice call is active. // Only one client is considered here for now. gFFI.chatModel.onVoiceCallClosed("End connetion"); + if (isAndroid) { + _textController.removeListener(textAndroidListener); + } + } + + // This listener is used to handle the composing region changes for Android soft keyboard input. + void textAndroidListener() { + if (_lastComposingChangeValid) { + _handleNonIOSSoftKeyboardInput(_textController.text); + } } // to-do: It should be better to use transparent color instead of the bgColor. @@ -150,7 +160,6 @@ class _RemotePageState extends State { gFFI.ffiModel.pi.version.isNotEmpty) { gFFI.invokeMethod("enable_soft_keyboard", false); } - _composingTimer?.cancel(); } else { _timer?.cancel(); _timer = Timer(kMobileDelaySoftKeyboardFocus, () { @@ -214,11 +223,8 @@ class _RemotePageState extends State { } void _handleNonIOSSoftKeyboardInput(String newValue) { - _composingTimer?.cancel(); - if (_textController.value.isComposingRangeValid) { - _composingTimer = Timer(Duration(milliseconds: 25), () { - _handleNonIOSSoftKeyboardInput(_textController.value.text); - }); + _lastComposingChangeValid = _textController.value.isComposingRangeValid; + if (_lastComposingChangeValid) { return; } var oldValue = _value; From bae4a2c71065e07b9149ef0f4ba5ff47dd6e5f6c Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Wed, 30 Oct 2024 15:29:52 +0800 Subject: [PATCH 340/541] Fix/android check normal usbhid usage (#9784) * fix: android check normal usbhid usage Signed-off-by: fufesou * fix: android input, ignore composing if is deleting Signed-off-by: fufesou --------- Signed-off-by: fufesou --- flutter/lib/mobile/pages/remote_page.dart | 4 +- flutter/lib/models/input_model.dart | 52 ++++++++++++++--------- 2 files changed, 36 insertions(+), 20 deletions(-) diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index a8446de20225..70ebb987797b 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -224,7 +224,9 @@ class _RemotePageState extends State { void _handleNonIOSSoftKeyboardInput(String newValue) { _lastComposingChangeValid = _textController.value.isComposingRangeValid; - if (_lastComposingChangeValid) { + if (_lastComposingChangeValid && newValue.length > _value.length) { + // Only early return if is composing new words. + // We need to send `backspace` immediately if is deleting letters. return; } var oldValue = _value; diff --git a/flutter/lib/models/input_model.dart b/flutter/lib/models/input_model.dart index 1b2a9736ee2a..3f413c499bf9 100644 --- a/flutter/lib/models/input_model.dart +++ b/flutter/lib/models/input_model.dart @@ -544,26 +544,40 @@ class InputModel { handleKeyDownEventModifiers(e); } - // The physicalKey.usbHidUsage may be not correct for soft keyboard on Android. - // iOS does not have this issue. - // 1. Open the soft keyboard on Android - // 2. Switch to input method like zh/ko/ja - // 3. Click Backspace and Enter on the soft keyboard or physical keyboard - // 4. The physicalKey.usbHidUsage is not correct. - // PhysicalKeyboardKey#8ac83(usbHidUsage: "0x1100000042", debugName: "Key with ID 0x1100000042") - // LogicalKeyboardKey#2604c(keyId: "0x10000000d", keyLabel: "Enter", debugName: "Enter") - // - // The correct PhysicalKeyboardKey should be - // PhysicalKeyboardKey#e14a9(usbHidUsage: "0x00070028", debugName: "Enter") - // https://github.com/flutter/flutter/issues/157771 - // We cannot use the debugName to determine the key is correct or not, because it's null in release mode. - // to-do: `isLegacyModeKeys` is not the best workaround, we need to find a better way to fix this issue. - final isLegacyModeKeys = ['Backspace', 'Enter'].contains(e.logicalKey.keyLabel); - final isMobileAndPeerNotAndroid = - isMobile && peerPlatform != kPeerPlatformAndroid; + bool isMobileAndMapMode = false; + if (isMobile) { + // Do not use map mode if mobile -> Android. Android does not support map mode for now. + // Because simulating the physical key events(uhid) which requires root permission is not supported. + if (peerPlatform != kPeerPlatformAndroid) { + if (isIOS) { + isMobileAndMapMode = true; + } else { + // The physicalKey.usbHidUsage may be not correct for soft keyboard on Android. + // iOS does not have this issue. + // 1. Open the soft keyboard on Android + // 2. Switch to input method like zh/ko/ja + // 3. Click Backspace and Enter on the soft keyboard or physical keyboard + // 4. The physicalKey.usbHidUsage is not correct. + // PhysicalKeyboardKey#8ac83(usbHidUsage: "0x1100000042", debugName: "Key with ID 0x1100000042") + // LogicalKeyboardKey#2604c(keyId: "0x10000000d", keyLabel: "Enter", debugName: "Enter") + // + // The correct PhysicalKeyboardKey should be + // PhysicalKeyboardKey#e14a9(usbHidUsage: "0x00070028", debugName: "Enter") + // https://github.com/flutter/flutter/issues/157771 + // We cannot use the debugName to determine the key is correct or not, because it's null in release mode. + // The normal `usbHidUsage` for keyboard shoud be between [0x00000010, 0x000c029f] + // https://github.com/flutter/flutter/blob/c051b69e2a2224300e20d93dbd15f4b91e8844d1/packages/flutter/lib/src/services/keyboard_key.g.dart#L5332 - 5600 + final isNormalHsbHidUsage = (e.physicalKey.usbHidUsage >> 20) == 0; + isMobileAndMapMode = isNormalHsbHidUsage && + // No need to check `!['Backspace', 'Enter'].contains(e.logicalKey.keyLabel)` + // But we still add it for more reliability. + !['Backspace', 'Enter'].contains(e.logicalKey.keyLabel); + } + } + } final isDesktopAndMapMode = - isDesktop || isWebDesktop && keyboardMode == kKeyMapMode; - if (!isLegacyModeKeys && (isMobileAndPeerNotAndroid || isDesktopAndMapMode)) { + isDesktop || (isWebDesktop && keyboardMode == kKeyMapMode); + if (isMobileAndMapMode || isDesktopAndMapMode) { // FIXME: e.character is wrong for dead keys, eg: ^ in de newKeyboardMode( e.character ?? '', From 32dbc0c8fb0245bd57249828a9f815c1bb5ae668 Mon Sep 17 00:00:00 2001 From: Kleofass <4000163+Kleofass@users.noreply.github.com> Date: Wed, 30 Oct 2024 11:08:32 +0200 Subject: [PATCH 341/541] Update lv.rs (#9785) --- src/lang/lv.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lang/lv.rs b/src/lang/lv.rs index e8ba903dfb66..4c78dfbd9613 100644 --- a/src/lang/lv.rs +++ b/src/lang/lv.rs @@ -364,7 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recording", "Ierakstīšana"), ("Directory", "Direktorija"), ("Automatically record incoming sessions", "Automātiski ierakstīt ienākošās sesijas"), - ("Automatically record outgoing sessions", ""), + ("Automatically record outgoing sessions", "Automātiski ierakstīt izejošās sesijas"), ("Change", "Mainīt"), ("Start session recording", "Sākt sesijas ierakstīšanu"), ("Stop session recording", "Apturēt sesijas ierakstīšanu"), From 0b8cccd8be93e83f54279ca61b0407d6c8c4d388 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Wed, 30 Oct 2024 19:00:07 +0800 Subject: [PATCH 342/541] fix: mobile view style, on conn (#9786) Signed-off-by: fufesou --- flutter/lib/models/model.dart | 8 +------- libs/hbb_common/src/config.rs | 3 +++ 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index bd844c3ca306..1e7baf6739de 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -1275,13 +1275,6 @@ class ImageModel with ChangeNotifier { if (isDesktop || isWebDesktop) { await parent.target?.canvasModel.updateViewStyle(); await parent.target?.canvasModel.updateScrollStyle(); - } else { - final size = MediaQueryData.fromWindow(ui.window).size; - final canvasWidth = size.width; - final canvasHeight = size.height; - final xscale = canvasWidth / image.width; - final yscale = canvasHeight / image.height; - parent.target?.canvasModel.scale = min(xscale, yscale); } if (parent.target != null) { await initializeCursorAndCanvas(parent.target!); @@ -1679,6 +1672,7 @@ class CanvasModel with ChangeNotifier { _x = 0; _y = 0; _scale = 1.0; + _lastViewStyle = ViewStyle.defaultViewStyle(); } updateScrollPercent() { diff --git a/libs/hbb_common/src/config.rs b/libs/hbb_common/src/config.rs index 73b330761a91..5807dafa5b4a 100644 --- a/libs/hbb_common/src/config.rs +++ b/libs/hbb_common/src/config.rs @@ -1710,6 +1710,9 @@ impl UserDefaultConfig { pub fn get(&self, key: &str) -> String { match key { + #[cfg(any(target_os = "android", target_os = "ios"))] + keys::OPTION_VIEW_STYLE => self.get_string(key, "adaptive", vec!["original"]), + #[cfg(not(any(target_os = "android", target_os = "ios")))] keys::OPTION_VIEW_STYLE => self.get_string(key, "original", vec!["adaptive"]), keys::OPTION_SCROLL_STYLE => self.get_string(key, "scrollauto", vec!["scrollbar"]), keys::OPTION_IMAGE_QUALITY => { From 697dd8738346cb3c72fbc6976e9fb45dfb826a58 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Thu, 31 Oct 2024 10:11:00 +0800 Subject: [PATCH 343/541] Refact/mobile remove adjust 4 soft keyabord (#9787) * refact: remove adjust for soft keyboard Signed-off-by: fufesou * mobile, do not set the view style after scale end Signed-off-by: fufesou --------- Signed-off-by: fufesou --- flutter/lib/common/widgets/remote_input.dart | 3 +- flutter/lib/mobile/pages/remote_page.dart | 22 ++++++++---- flutter/lib/models/model.dart | 37 ++++++-------------- 3 files changed, 29 insertions(+), 33 deletions(-) diff --git a/flutter/lib/common/widgets/remote_input.dart b/flutter/lib/common/widgets/remote_input.dart index 4a82dfae3262..bea1490fe5c5 100644 --- a/flutter/lib/common/widgets/remote_input.dart +++ b/flutter/lib/common/widgets/remote_input.dart @@ -336,7 +336,8 @@ class _RawTouchGestureDetectorRegionState } else { // mobile _scale = 1; - bind.sessionSetViewStyle(sessionId: sessionId, value: ""); + // No idea why we need to set the view style to "" here. + // bind.sessionSetViewStyle(sessionId: sessionId, value: ""); } inputModel.sendMouse('up', MouseButtons.left); } diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index 70ebb987797b..2e6004faa87e 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -37,13 +37,15 @@ class RemotePage extends StatefulWidget { State createState() => _RemotePageState(id); } -class _RemotePageState extends State { +class _RemotePageState extends State with WidgetsBindingObserver { Timer? _timer; bool _showBar = !isWebDesktop; bool _showGestureHelp = false; String _value = ''; Orientation? _currentOrientation; + Timer? _timerDidChangeMetrics; + final _blockableOverlayState = BlockableOverlayState(); final keyboardVisibilityController = KeyboardVisibilityController(); @@ -100,10 +102,12 @@ class _RemotePageState extends State { if (isAndroid) { _textController.addListener(textAndroidListener); } + WidgetsBinding.instance.addObserver(this); } @override Future dispose() async { + WidgetsBinding.instance.removeObserver(this); // https://github.com/flutter/flutter/issues/64935 super.dispose(); gFFI.dialogManager.hideMobileActionsOverlay(store: false); @@ -115,6 +119,7 @@ class _RemotePageState extends State { _physicalFocusNode.dispose(); await gFFI.close(); _timer?.cancel(); + _timerDidChangeMetrics?.cancel(); gFFI.dialogManager.dismissAll(); await SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: SystemUiOverlay.values); @@ -132,6 +137,14 @@ class _RemotePageState extends State { } } + @override + void didChangeMetrics() { + _timerDidChangeMetrics?.cancel(); + _timerDidChangeMetrics = Timer(Duration(milliseconds: 100), () { + gFFI.canvasModel.updateViewStyle(refreshMousePos: false); + }); + } + // This listener is used to handle the composing region changes for Android soft keyboard input. void textAndroidListener() { if (_lastComposingChangeValid) { @@ -968,11 +981,9 @@ class ImagePaint extends StatelessWidget { Widget build(BuildContext context) { final m = Provider.of(context); final c = Provider.of(context); - final adjust = gFFI.cursorModel.adjustForKeyboard(); var s = c.scale; return CustomPaint( - painter: ImagePainter( - image: m.image, x: c.x / s, y: (c.y - adjust) / s, scale: s), + painter: ImagePainter(image: m.image, x: c.x / s, y: c.y / s, scale: s), ); } } @@ -986,7 +997,6 @@ class CursorPaint extends StatelessWidget { final m = Provider.of(context); final c = Provider.of(context); final ffiModel = Provider.of(context); - final adjust = gFFI.cursorModel.adjustForKeyboard(); final s = c.scale; double hotx = m.hotx; double hoty = m.hoty; @@ -1022,7 +1032,7 @@ class CursorPaint extends StatelessWidget { painter: ImagePainter( image: image, x: (m.x - hotx) * factor + c.x / s2, - y: (m.y - hoty) * factor + (c.y - adjust) / s2, + y: (m.y - hoty) * factor + c.y / s2, scale: s2), ); } diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 1e7baf6739de..e55a50636061 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -1466,10 +1466,14 @@ class CanvasModel with ChangeNotifier { updateViewStyle({refreshMousePos = true}) async { Size getSize() { - final size = MediaQueryData.fromWindow(ui.window).size; + final mediaData = MediaQueryData.fromView(ui.window); + final size = mediaData.size; // If minimized, w or h may be negative here. double w = size.width - leftToEdge - rightToEdge; double h = size.height - topToEdge - bottomToEdge; + if (isMobile) { + h -= (mediaData.padding.top + mediaData.viewInsets.bottom); + } return Size(w < 0 ? 0 : w, h < 0 ? 0 : h); } @@ -1643,13 +1647,9 @@ class CanvasModel with ChangeNotifier { // (focalPoint.dx - _x_1) / s1 + displayOriginX = (focalPoint.dx - _x_2) / s2 + displayOriginX // _x_2 = focalPoint.dx - (focalPoint.dx - _x_1) / s1 * s2 _x = focalPoint.dx - (focalPoint.dx - _x) / s * _scale; - final adjustForKeyboard = - parent.target?.cursorModel.adjustForKeyboard() ?? 0.0; - // (focalPoint.dy - _y_1 + adjust) / s1 + displayOriginY = (focalPoint.dy - _y_2 + adjust) / s2 + displayOriginY - // _y_2 = focalPoint.dy + adjust - (focalPoint.dy - _y_1 + adjust) / s1 * s2 - _y = focalPoint.dy + - adjustForKeyboard - - (focalPoint.dy - _y + adjustForKeyboard) / s * _scale; + // (focalPoint.dy - _y_1) / s1 + displayOriginY = (focalPoint.dy - _y_2) / s2 + displayOriginY + // _y_2 = focalPoint.dy - (focalPoint.dy - _y_1) / s1 * s2 + _y = focalPoint.dy - (focalPoint.dy - _y) / s * _scale; notifyListeners(); } @@ -1883,7 +1883,6 @@ class CursorModel with ChangeNotifier { // `lastIsBlocked` is only used in common/widgets/remote_input.dart -> _RawTouchGestureDetectorRegionState -> onDoubleTap() // Because onDoubleTap() doesn't have the `event` parameter, we can't get the touch event's position. bool _lastIsBlocked = false; - double _yForKeyboardAdjust = 0; keyHelpToolsVisibilityChanged(Rect? r) { _keyHelpToolsRect = r; @@ -1895,7 +1894,6 @@ class CursorModel with ChangeNotifier { // `lastIsBlocked` will be set when the cursor is moving or touch somewhere else. _lastIsBlocked = true; } - _yForKeyboardAdjust = _y; } get lastIsBlocked => _lastIsBlocked; @@ -1947,19 +1945,6 @@ class CursorModel with ChangeNotifier { get keyboardHeight => MediaQueryData.fromWindow(ui.window).viewInsets.bottom; get scale => parent.target?.canvasModel.scale ?? 1.0; - double adjustForKeyboard() { - if (keyboardHeight < 100) { - return 0.0; - } - - final m = MediaQueryData.fromWindow(ui.window); - final size = m.size; - final thresh = (size.height - keyboardHeight) / 2; - final h = (_yForKeyboardAdjust - getVisibleRect().top) * - scale; // local physical display height - return h - thresh; - } - // mobile Soft keyboard, block touch event from the KeyHelpTools shouldBlock(double x, double y) { if (!(parent.target?.ffiModel.touchMode ?? false)) { @@ -1980,16 +1965,16 @@ class CursorModel with ChangeNotifier { return false; } _lastIsBlocked = false; - moveLocal(x, y, adjust: adjustForKeyboard()); + moveLocal(x, y); parent.target?.inputModel.moveMouse(_x, _y); return true; } - moveLocal(double x, double y, {double adjust = 0}) { + moveLocal(double x, double y) { final xoffset = parent.target?.canvasModel.x ?? 0; final yoffset = parent.target?.canvasModel.y ?? 0; _x = (x - xoffset) / scale + _displayOriginX; - _y = (y - yoffset + adjust) / scale + _displayOriginY; + _y = (y - yoffset) / scale + _displayOriginY; notifyListeners(); } From f86c88b3d81766a5c6073c321ee56c310beb48eb Mon Sep 17 00:00:00 2001 From: 21pages Date: Thu, 31 Oct 2024 10:11:42 +0800 Subject: [PATCH 344/541] refresh icon not visible when not using one-time password (#9791) Signed-off-by: 21pages --- .../lib/desktop/pages/desktop_home_page.dart | 49 ++++++++++++------- flutter/lib/mobile/pages/server_page.dart | 7 +-- src/ui/index.tis | 5 +- 3 files changed, 37 insertions(+), 24 deletions(-) diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index 493e4ca47bb6..9728d6b478e2 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -272,10 +272,21 @@ class _DesktopHomePageState extends State } buildPasswordBoard(BuildContext context) { - final model = gFFI.serverModel; + return ChangeNotifierProvider.value( + value: gFFI.serverModel, + child: Consumer( + builder: (context, model, child) { + return buildPasswordBoard2(context, model); + }, + )); + } + + buildPasswordBoard2(BuildContext context, ServerModel model) { RxBool refreshHover = false.obs; RxBool editHover = false.obs; final textColor = Theme.of(context).textTheme.titleLarge?.color; + final showOneTime = model.approveMode != 'click' && + model.verificationMethod != kUsePermanentPassword; return Container( margin: EdgeInsets.only(left: 20.0, right: 16, top: 13, bottom: 13), child: Row( @@ -304,8 +315,7 @@ class _DesktopHomePageState extends State Expanded( child: GestureDetector( onDoubleTap: () { - if (model.verificationMethod != - kUsePermanentPassword) { + if (showOneTime) { Clipboard.setData( ClipboardData(text: model.serverPasswd.text)); showToast(translate("Copied")); @@ -323,22 +333,23 @@ class _DesktopHomePageState extends State ), ), ), - AnimatedRotationWidget( - onPressed: () => bind.mainUpdateTemporaryPassword(), - child: Tooltip( - message: translate('Refresh Password'), - child: Obx(() => RotatedBox( - quarterTurns: 2, - child: Icon( - Icons.refresh, - color: refreshHover.value - ? textColor - : Color(0xFFDDDDDD), - size: 22, - ))), - ), - onHover: (value) => refreshHover.value = value, - ).marginOnly(right: 8, top: 4), + if (showOneTime) + AnimatedRotationWidget( + onPressed: () => bind.mainUpdateTemporaryPassword(), + child: Tooltip( + message: translate('Refresh Password'), + child: Obx(() => RotatedBox( + quarterTurns: 2, + child: Icon( + Icons.refresh, + color: refreshHover.value + ? textColor + : Color(0xFFDDDDDD), + size: 22, + ))), + ), + onHover: (value) => refreshHover.value = value, + ).marginOnly(right: 8, top: 4), if (!bind.isDisableSettings()) InkWell( child: Tooltip( diff --git a/flutter/lib/mobile/pages/server_page.dart b/flutter/lib/mobile/pages/server_page.dart index 06258e5a5ae0..d6710b43d6a3 100644 --- a/flutter/lib/mobile/pages/server_page.dart +++ b/flutter/lib/mobile/pages/server_page.dart @@ -446,7 +446,6 @@ class ServerInfo extends StatelessWidget { @override Widget build(BuildContext context) { - final isPermanent = model.verificationMethod == kUsePermanentPassword; final serverModel = Provider.of(context); const Color colorPositive = Colors.green; @@ -486,6 +485,8 @@ class ServerInfo extends StatelessWidget { } } + final showOneTime = serverModel.approveMode != 'click' && + serverModel.verificationMethod != kUsePermanentPassword; return PaddingCard( title: translate('Your Device'), child: Column( @@ -523,10 +524,10 @@ class ServerInfo extends StatelessWidget { ]), Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - isPermanent ? '-' : model.serverPasswd.value.text, + !showOneTime ? '-' : model.serverPasswd.value.text, style: textStyleValue, ), - isPermanent + !showOneTime ? SizedBox.shrink() : Row(children: [ IconButton( diff --git a/src/ui/index.tis b/src/ui/index.tis index 3ae54637f4b3..2c9b0f9835bc 100644 --- a/src/ui/index.tis +++ b/src/ui/index.tis @@ -831,11 +831,12 @@ class PasswordEyeArea : Reactor.Component { render() { var method = handler.get_option('verification-method'); var mode= handler.get_option('approve-mode'); - var value = mode == 'click' || method == 'use-permanent-password' ? "-" : password_cache[0]; + var hide_one_time = mode == 'click' || method == 'use-permanent-password'; + var value = hide_one_time ? "-" : password_cache[0]; return
    - {svg_refresh_password} + {hide_one_time ? "" : svg_refresh_password}
    ; } From 4c12b83068df869c4d4c372b0c6d1edeb9233502 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Thu, 31 Oct 2024 10:20:57 +0800 Subject: [PATCH 345/541] fix: android input do not handle composing (#9790) Signed-off-by: fufesou --- flutter/lib/mobile/pages/remote_page.dart | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index 2e6004faa87e..437edad90d9f 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -59,7 +59,6 @@ class _RemotePageState extends State with WidgetsBindingObserver { final TextEditingController _textController = TextEditingController(text: initText); - bool _lastComposingChangeValid = false; _RemotePageState(String id) { initSharedStates(id); @@ -99,9 +98,6 @@ class _RemotePageState extends State with WidgetsBindingObserver { showToast(translate('Automatically record outgoing sessions')); } }); - if (isAndroid) { - _textController.addListener(textAndroidListener); - } WidgetsBinding.instance.addObserver(this); } @@ -132,9 +128,6 @@ class _RemotePageState extends State with WidgetsBindingObserver { // The inner logic of `on_voice_call_closed` will check if the voice call is active. // Only one client is considered here for now. gFFI.chatModel.onVoiceCallClosed("End connetion"); - if (isAndroid) { - _textController.removeListener(textAndroidListener); - } } @override @@ -145,13 +138,6 @@ class _RemotePageState extends State with WidgetsBindingObserver { }); } - // This listener is used to handle the composing region changes for Android soft keyboard input. - void textAndroidListener() { - if (_lastComposingChangeValid) { - _handleNonIOSSoftKeyboardInput(_textController.text); - } - } - // to-do: It should be better to use transparent color instead of the bgColor. // But for now, the transparent color will cause the canvas to be white. // I'm sure that the white color is caused by the Overlay widget in BlockableOverlay. @@ -236,12 +222,6 @@ class _RemotePageState extends State with WidgetsBindingObserver { } void _handleNonIOSSoftKeyboardInput(String newValue) { - _lastComposingChangeValid = _textController.value.isComposingRangeValid; - if (_lastComposingChangeValid && newValue.length > _value.length) { - // Only early return if is composing new words. - // We need to send `backspace` immediately if is deleting letters. - return; - } var oldValue = _value; _value = newValue; if (oldValue.isNotEmpty && From d1fdcf1b16c440d2af69cc72a344792eaa0e7c6d Mon Sep 17 00:00:00 2001 From: jkh0kr Date: Thu, 31 Oct 2024 13:01:37 +0900 Subject: [PATCH 346/541] Update ko.rs (#9792) --- src/lang/ko.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 71bff5119085..9b8c08d71171 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -648,10 +648,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("one-way-file-transfer-tip", "단방향 파일 전송은 제어되는 쪽에서 활성화됩니다."), ("Authentication Required", "인증 필요함"), ("Authenticate", "인증"), - ("web_id_input_tip", ""), - ("Download", ""), - ("Upload folder", ""), - ("Upload files", ""), - ("Clipboard is synchronized", ""), + ("web_id_input_tip", "동일한 서버에 있는 ID를 입력할 수 있습니다. 웹 클라이언트에서는 직접 IP 접속이 지원되지 않습니다.\n 다른 서버에 있는 장치에 접속하려면 서버 주소(@?key=)를 추가하세요. 예:\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\n 공용 서버에 있는 장치에 접속하려면 \"@public\"을 입력하세요. 공용 서버에서는 키가 필요하지 않습니다."), + ("Download", "다운로드"), + ("Upload folder", "폴더 업로드"), + ("Upload files", "파일 업로드"), + ("Clipboard is synchronized", "클립보드가 동기화됨"), ].iter().cloned().collect(); } From 4f7e10bac6d09f9364edcd2983ce36b0a2e90cb9 Mon Sep 17 00:00:00 2001 From: 21pages Date: Thu, 31 Oct 2024 22:57:39 +0800 Subject: [PATCH 347/541] Revert "Change the minimum value of the bitrate slider to 5" (#9795) * Revert "Change the minimum value of the bitrate slider to 5 (#9480)" This reverts commit beb1084e877f0ae302945f912a3ccf5aaf59ea84. * Revert "Change the value of kMinQuality to 5 (#9508)" This reverts commit d563372a91a1b1dcb552f6571828ccf081ea3f42. --- flutter/lib/consts.dart | 4 ++-- src/ui/header.tis | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index 89306bb7ae89..352da9e6fa66 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -202,7 +202,7 @@ const double kMinFps = 5; const double kDefaultFps = 30; const double kMaxFps = 120; -const double kMinQuality = 5; +const double kMinQuality = 10; const double kDefaultQuality = 50; const double kMaxQuality = 100; const double kMaxMoreQuality = 2000; @@ -572,4 +572,4 @@ extension WindowsTargetExt on int { WindowsTarget get windowsVersion => getWindowsTarget(this); } -const kCheckSoftwareUpdateFinish = 'check_software_update_finish'; +const kCheckSoftwareUpdateFinish = 'check_software_update_finish'; \ No newline at end of file diff --git a/src/ui/header.tis b/src/ui/header.tis index 3116f1f542fa..4b634cf54c5a 100644 --- a/src/ui/header.tis +++ b/src/ui/header.tis @@ -432,7 +432,7 @@ function handle_custom_image_quality() { var extendedBitrate = bitrate > 100; var maxRate = extendedBitrate ? 2000 : 100; msgbox("custom-image-quality", "Custom Image Quality", "
    \ -
    x% Bitrate More
    \ +
    x% Bitrate More
    \
    ", "", function(res=null) { if (!res) return; if (res.id === "extended-slider") { From 44fa83d0805154bfa80a3913c5f1801ae0fd4660 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Fri, 1 Nov 2024 11:25:38 +0800 Subject: [PATCH 348/541] fix: android input, soft keyboard, mouse mode (#9797) Cursor movement in the remote screen. Signed-off-by: fufesou --- flutter/lib/mobile/pages/remote_page.dart | 19 +++- flutter/lib/models/model.dart | 107 +++++++++++++++++----- 2 files changed, 99 insertions(+), 27 deletions(-) diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index 437edad90d9f..6de87b43b216 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:ui' as ui; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -43,6 +44,7 @@ class _RemotePageState extends State with WidgetsBindingObserver { bool _showGestureHelp = false; String _value = ''; Orientation? _currentOrientation; + double _viewInsetsBottom = 0; Timer? _timerDidChangeMetrics; @@ -132,9 +134,15 @@ class _RemotePageState extends State with WidgetsBindingObserver { @override void didChangeMetrics() { + final newBottom = MediaQueryData.fromView(ui.window).viewInsets.bottom; _timerDidChangeMetrics?.cancel(); - _timerDidChangeMetrics = Timer(Duration(milliseconds: 100), () { - gFFI.canvasModel.updateViewStyle(refreshMousePos: false); + _timerDidChangeMetrics = Timer(Duration(milliseconds: 100), () async { + // We need this comparation because poping up the floating action will also trigger `didChangeMetrics()`. + if (newBottom != _viewInsetsBottom) { + await gFFI.canvasModel.updateViewStyle(refreshMousePos: false); + gFFI.canvasModel.moveToCenterCursor(); + _viewInsetsBottom = newBottom; + } }); } @@ -962,8 +970,10 @@ class ImagePaint extends StatelessWidget { final m = Provider.of(context); final c = Provider.of(context); var s = c.scale; + final adjust = c.getAdjustY(); return CustomPaint( - painter: ImagePainter(image: m.image, x: c.x / s, y: c.y / s, scale: s), + painter: ImagePainter( + image: m.image, x: c.x / s, y: (c.y + adjust) / s, scale: s), ); } } @@ -1008,11 +1018,12 @@ class CursorPaint extends StatelessWidget { factor = s / mins; } final s2 = s < mins ? mins : s; + final adjust = c.getAdjustY(); return CustomPaint( painter: ImagePainter( image: image, x: (m.x - hotx) * factor + c.x / s2, - y: (m.y - hoty) * factor + c.y / s2, + y: (m.y - hoty) * factor + (c.y + adjust) / s2, scale: s2), ); } diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index e55a50636061..d7462a2121c4 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -1286,20 +1286,18 @@ class ImageModel with ChangeNotifier { } // mobile only - // for desktop, height should minus tabbar height double get maxScale { if (_image == null) return 1.5; - final size = MediaQueryData.fromWindow(ui.window).size; + final size = parent.target!.canvasModel.getSize(); final xscale = size.width / _image!.width; final yscale = size.height / _image!.height; return max(1.5, max(xscale, yscale)); } // mobile only - // for desktop, height should minus tabbar height double get minScale { if (_image == null) return 1.5; - final size = MediaQueryData.fromWindow(ui.window).size; + final size = parent.target!.canvasModel.getSize(); final xscale = size.width / _image!.width; final yscale = size.height / _image!.height; return min(xscale, yscale) / 1.5; @@ -1464,19 +1462,27 @@ class CanvasModel with ChangeNotifier { static double get bottomToEdge => isDesktop ? windowBorderWidth + kDragToResizeAreaPadding.bottom : 0; - updateViewStyle({refreshMousePos = true}) async { - Size getSize() { - final mediaData = MediaQueryData.fromView(ui.window); - final size = mediaData.size; - // If minimized, w or h may be negative here. - double w = size.width - leftToEdge - rightToEdge; - double h = size.height - topToEdge - bottomToEdge; - if (isMobile) { - h -= (mediaData.padding.top + mediaData.viewInsets.bottom); - } - return Size(w < 0 ? 0 : w, h < 0 ? 0 : h); + Size getSize() { + final mediaData = MediaQueryData.fromView(ui.window); + final size = mediaData.size; + // If minimized, w or h may be negative here. + double w = size.width - leftToEdge - rightToEdge; + double h = size.height - topToEdge - bottomToEdge; + if (isMobile) { + h = h - + mediaData.viewInsets.bottom - + (parent.target?.cursorModel.keyHelpToolsRect?.bottom ?? 0); } + return Size(w < 0 ? 0 : w, h < 0 ? 0 : h); + } + + // mobile only + double getAdjustY() { + final bottom = parent.target?.cursorModel.keyHelpToolsRect?.bottom ?? 0; + return max(bottom - MediaQueryData.fromView(ui.window).padding.top, 0); + } + updateViewStyle({refreshMousePos = true}) async { final style = await bind.sessionGetViewStyle(sessionId: sessionId); if (style == null) { return; @@ -1636,6 +1642,7 @@ class CanvasModel with ChangeNotifier { notifyListeners(); } + // mobile only updateScale(double v, Offset focalPoint) { if (parent.target?.imageModel.image == null) return; final s = _scale; @@ -1647,9 +1654,10 @@ class CanvasModel with ChangeNotifier { // (focalPoint.dx - _x_1) / s1 + displayOriginX = (focalPoint.dx - _x_2) / s2 + displayOriginX // _x_2 = focalPoint.dx - (focalPoint.dx - _x_1) / s1 * s2 _x = focalPoint.dx - (focalPoint.dx - _x) / s * _scale; - // (focalPoint.dy - _y_1) / s1 + displayOriginY = (focalPoint.dy - _y_2) / s2 + displayOriginY - // _y_2 = focalPoint.dy - (focalPoint.dy - _y_1) / s1 * s2 - _y = focalPoint.dy - (focalPoint.dy - _y) / s * _scale; + final adjust = getAdjustY(); + // (focalPoint.dy - _y_1 - adjust) / s1 + displayOriginY = (focalPoint.dy - _y_2 - adjust) / s2 + displayOriginY + // _y_2 = focalPoint.dy - adjust - (focalPoint.dy - _y_1 - adjust) / s1 * s2 + _y = focalPoint.dy - adjust - (focalPoint.dy - _y - adjust) / s * _scale; notifyListeners(); } @@ -1690,6 +1698,34 @@ class CanvasModel with ChangeNotifier { : 0.0; setScrollPercent(percentX, percentY); } + + // mobile only + // Move the canvas to make the cursor visible(center) on the screen. + void moveToCenterCursor() { + Rect? imageRect = parent.target?.ffiModel.rect; + if (imageRect == null) { + // unreachable + return; + } + final maxX = 0.0; + final minX = _size.width + (imageRect.left - imageRect.right) * _scale; + final maxY = 0.0; + final minY = _size.height + (imageRect.top - imageRect.bottom) * _scale; + Offset offsetToCenter = + parent.target?.cursorModel.getCanvasOffsetToCenterCursor() ?? + Offset.zero; + if (minX < 0) { + _x = min(max(offsetToCenter.dx, minX), maxX); + } else { + // _size.width > (imageRect.right, imageRect.left) * _scale, we should not change _x + } + if (minY < 0) { + _y = min(max(offsetToCenter.dy, minY), maxY); + } else { + // _size.height > (imageRect.bottom - imageRect.top) * _scale, , we should not change _y + } + notifyListeners(); + } } // data for cursor @@ -1884,6 +1920,7 @@ class CursorModel with ChangeNotifier { // Because onDoubleTap() doesn't have the `event` parameter, we can't get the touch event's position. bool _lastIsBlocked = false; + Rect? get keyHelpToolsRect => _keyHelpToolsRect; keyHelpToolsVisibilityChanged(Rect? r) { _keyHelpToolsRect = r; if (r == null) { @@ -1894,6 +1931,15 @@ class CursorModel with ChangeNotifier { // `lastIsBlocked` will be set when the cursor is moving or touch somewhere else. _lastIsBlocked = true; } + if (isMobile) { + if (r != null || _lastIsBlocked) { + () async { + await parent.target?.canvasModel + .updateViewStyle(refreshMousePos: false); + parent.target?.canvasModel.moveToCenterCursor(); + }(); + } + } } get lastIsBlocked => _lastIsBlocked; @@ -1932,8 +1978,10 @@ class CursorModel with ChangeNotifier { addKey(String key) => _cacheKeys.add(key); // remote physical display coordinate + // For update pan (mobile), onOneFingerPanStart, onOneFingerPanUpdate, onHoldDragUpdate Rect getVisibleRect() { - final size = MediaQueryData.fromWindow(ui.window).size; + final size = parent.target?.canvasModel.getSize() ?? + MediaQueryData.fromView(ui.window).size; final xoffset = parent.target?.canvasModel.x ?? 0; final yoffset = parent.target?.canvasModel.y ?? 0; final scale = parent.target?.canvasModel.scale ?? 1; @@ -1942,7 +1990,20 @@ class CursorModel with ChangeNotifier { return Rect.fromLTWH(x0, y0, size.width / scale, size.height / scale); } - get keyboardHeight => MediaQueryData.fromWindow(ui.window).viewInsets.bottom; + Offset getCanvasOffsetToCenterCursor() { + // cursor should be in the center of the visible rect + // _x = rect.left + rect.width / 2 + // _y = rect.right + rect.height / 2 + // See `getVisibleRect()` + // _x = _displayOriginX - xoffset / scale + size.width / scale * 0.5; + // _y = _displayOriginY - yoffset / scale + size.height / scale * 0.5; + final size = parent.target?.canvasModel.getSize() ?? + MediaQueryData.fromView(ui.window).size; + final xoffset = (_displayOriginX - _x) * scale + size.width * 0.5; + final yoffset = (_displayOriginY - _y) * scale + size.height * 0.5; + return Offset(xoffset, yoffset); + } + get scale => parent.target?.canvasModel.scale ?? 1.0; // mobile Soft keyboard, block touch event from the KeyHelpTools @@ -1965,16 +2026,16 @@ class CursorModel with ChangeNotifier { return false; } _lastIsBlocked = false; - moveLocal(x, y); + moveLocal(x, y, adjust: parent.target?.canvasModel.getAdjustY() ?? 0); parent.target?.inputModel.moveMouse(_x, _y); return true; } - moveLocal(double x, double y) { + moveLocal(double x, double y, {double adjust = 0}) { final xoffset = parent.target?.canvasModel.x ?? 0; final yoffset = parent.target?.canvasModel.y ?? 0; _x = (x - xoffset) / scale + _displayOriginX; - _y = (y - yoffset) / scale + _displayOriginY; + _y = (y - yoffset - adjust) / scale + _displayOriginY; notifyListeners(); } From 040253b3199ee9fe95a4d91f16634a9504b83ce0 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Fri, 1 Nov 2024 15:40:57 +0800 Subject: [PATCH 349/541] fix: mobile cursor focus (#9803) Signed-off-by: fufesou --- flutter/lib/mobile/pages/remote_page.dart | 3 +- flutter/lib/models/model.dart | 42 +++++++++++++++++------ 2 files changed, 33 insertions(+), 12 deletions(-) diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index 6de87b43b216..bdee525fda33 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -139,8 +139,7 @@ class _RemotePageState extends State with WidgetsBindingObserver { _timerDidChangeMetrics = Timer(Duration(milliseconds: 100), () async { // We need this comparation because poping up the floating action will also trigger `didChangeMetrics()`. if (newBottom != _viewInsetsBottom) { - await gFFI.canvasModel.updateViewStyle(refreshMousePos: false); - gFFI.canvasModel.moveToCenterCursor(); + gFFI.canvasModel.mobileFocusCanvasCursor(); _viewInsetsBottom = newBottom; } }); diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index d7462a2121c4..80a6809d5181 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -1420,6 +1420,8 @@ class CanvasModel with ChangeNotifier { ScrollStyle _scrollStyle = ScrollStyle.scrollauto; ViewStyle _lastViewStyle = ViewStyle.defaultViewStyle(); + Timer? _timerMobileFocusCanvasCursor; + final ScrollController _horizontal = ScrollController(); final ScrollController _vertical = ScrollController(); @@ -1482,7 +1484,7 @@ class CanvasModel with ChangeNotifier { return max(bottom - MediaQueryData.fromView(ui.window).padding.top, 0); } - updateViewStyle({refreshMousePos = true}) async { + updateViewStyle({refreshMousePos = true, notify = true}) async { final style = await bind.sessionGetViewStyle(sessionId: sessionId); if (style == null) { return; @@ -1514,7 +1516,9 @@ class CanvasModel with ChangeNotifier { _x = (size.width - displayWidth * _scale) / 2; _y = (size.height - displayHeight * _scale) / 2; _imageOverflow.value = _x < 0 || y < 0; - notifyListeners(); + if (notify) { + notifyListeners(); + } if (refreshMousePos) { parent.target?.inputModel.refreshMousePos(); } @@ -1681,6 +1685,7 @@ class CanvasModel with ChangeNotifier { _y = 0; _scale = 1.0; _lastViewStyle = ViewStyle.defaultViewStyle(); + _timerMobileFocusCanvasCursor?.cancel(); } updateScrollPercent() { @@ -1699,9 +1704,20 @@ class CanvasModel with ChangeNotifier { setScrollPercent(percentX, percentY); } + void mobileFocusCanvasCursor() { + _timerMobileFocusCanvasCursor?.cancel(); + _timerMobileFocusCanvasCursor = + Timer(Duration(milliseconds: 100), () async { + await updateViewStyle(refreshMousePos: false, notify: false); + _moveToCenterCursor(); + parent.target?.cursorModel.ensureCursorInVisibleRect(); + notifyListeners(); + }); + } + // mobile only // Move the canvas to make the cursor visible(center) on the screen. - void moveToCenterCursor() { + void _moveToCenterCursor() { Rect? imageRect = parent.target?.ffiModel.rect; if (imageRect == null) { // unreachable @@ -1724,7 +1740,6 @@ class CanvasModel with ChangeNotifier { } else { // _size.height > (imageRect.bottom - imageRect.top) * _scale, , we should not change _y } - notifyListeners(); } } @@ -1933,11 +1948,7 @@ class CursorModel with ChangeNotifier { } if (isMobile) { if (r != null || _lastIsBlocked) { - () async { - await parent.target?.canvasModel - .updateViewStyle(refreshMousePos: false); - parent.target?.canvasModel.moveToCenterCursor(); - }(); + parent.target?.canvasModel.mobileFocusCanvasCursor(); } } } @@ -1991,7 +2002,7 @@ class CursorModel with ChangeNotifier { } Offset getCanvasOffsetToCenterCursor() { - // cursor should be in the center of the visible rect + // Cursor should be at the center of the visible rect. // _x = rect.left + rect.width / 2 // _y = rect.right + rect.height / 2 // See `getVisibleRect()` @@ -2004,6 +2015,17 @@ class CursorModel with ChangeNotifier { return Offset(xoffset, yoffset); } + void ensureCursorInVisibleRect() { + final ensureVisibleValue = 50.0; + final r = getVisibleRect(); + final minX = r.left; + final maxX = max(r.right - ensureVisibleValue, r.left); + final minY = r.top; + final maxY = max(r.bottom - ensureVisibleValue, minY); + _x = min(max(_x, minX), maxX); + _y = min(max(_y, minY), maxY); + } + get scale => parent.target?.canvasModel.scale ?? 1.0; // mobile Soft keyboard, block touch event from the KeyHelpTools From 12c1337b7bd3815a5d1b0d2affd838f509eff487 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Mon, 4 Nov 2024 22:37:21 +0800 Subject: [PATCH 350/541] fix: mobile mouse mode, cursor range (#9811) Signed-off-by: fufesou --- flutter/lib/common/widgets/gestures.dart | 2 +- flutter/lib/consts.dart | 7 +++ flutter/lib/models/input_model.dart | 63 +++++++++++++++++------- flutter/lib/models/model.dart | 49 +++++++++++------- 4 files changed, 84 insertions(+), 37 deletions(-) diff --git a/flutter/lib/common/widgets/gestures.dart b/flutter/lib/common/widgets/gestures.dart index 6c269656722f..b3cfeae6e632 100644 --- a/flutter/lib/common/widgets/gestures.dart +++ b/flutter/lib/common/widgets/gestures.dart @@ -86,7 +86,7 @@ class CustomTouchGestureRecognizer extends ScaleGestureRecognizer { // end switch (_currentState) { case GestureState.oneFingerPan: - debugPrint("TwoFingerState.pan onEnd"); + debugPrint("OneFingerState.pan onEnd"); if (onOneFingerPanEnd != null) { onOneFingerPanEnd!(_getDragEndDetails(d)); } diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index 352da9e6fa66..c313958bddfc 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -169,6 +169,13 @@ const int kWindowMainId = 0; const String kPointerEventKindTouch = "touch"; const String kPointerEventKindMouse = "mouse"; +const String kMouseEventTypeDefault = ""; +const String kMouseEventTypePanStart = "pan_start"; +const String kMouseEventTypePanUpdate = "pan_update"; +const String kMouseEventTypePanEnd = "pan_end"; +const String kMouseEventTypeDown = "down"; +const String kMouseEventTypeUp = "up"; + const String kKeyFlutterKey = "flutter_key"; const String kKeyShowDisplaysAsIndividualWindows = diff --git a/flutter/lib/models/input_model.dart b/flutter/lib/models/input_model.dart index 3f413c499bf9..0cc0537d67bb 100644 --- a/flutter/lib/models/input_model.dart +++ b/flutter/lib/models/input_model.dart @@ -856,7 +856,7 @@ class InputModel { _stopFling = true; if (isViewOnly) return; if (peerPlatform == kPeerPlatformAndroid) { - handlePointerEvent('touch', 'pan_start', e.position); + handlePointerEvent('touch', kMouseEventTypePanStart, e.position); } } @@ -899,8 +899,8 @@ class InputModel { } if (x != 0 || y != 0) { if (peerPlatform == kPeerPlatformAndroid) { - handlePointerEvent( - 'touch', 'pan_update', Offset(x.toDouble(), y.toDouble())); + handlePointerEvent('touch', kMouseEventTypePanUpdate, + Offset(x.toDouble(), y.toDouble())); } else { bind.sessionSendMouse( sessionId: sessionId, @@ -962,7 +962,7 @@ class InputModel { void onPointerPanZoomEnd(PointerPanZoomEndEvent e) { if (peerPlatform == kPeerPlatformAndroid) { - handlePointerEvent('touch', 'pan_end', e.position); + handlePointerEvent('touch', kMouseEventTypePanEnd, e.position); return; } @@ -1080,7 +1080,7 @@ class InputModel { onExit: true, ); - int trySetNearestRange(int v, int min, int max, int n) { + static int tryGetNearestRange(int v, int min, int max, int n) { if (v < min && v >= min - n) { v = min; } @@ -1120,13 +1120,13 @@ class InputModel { // to-do: handle mouse events late final dynamic evtValue; - if (type == 'pan_update') { + if (type == kMouseEventTypePanUpdate) { evtValue = { 'x': x.toInt(), 'y': y.toInt(), }; } else { - final isMoveTypes = ['pan_start', 'pan_end']; + final isMoveTypes = [kMouseEventTypePanStart, kMouseEventTypePanEnd]; final pos = handlePointerDevicePos( kPointerEventKindTouch, x, @@ -1181,14 +1181,14 @@ class InputModel { return; } - var type = ''; + var type = kMouseEventTypeDefault; var isMove = false; switch (evt['type']) { case _kMouseEventDown: - type = 'down'; + type = kMouseEventTypeDown; break; case _kMouseEventUp: - type = 'up'; + type = kMouseEventTypeUp; break; case _kMouseEventMove: _pointerMovedAfterEnter = true; @@ -1199,7 +1199,7 @@ class InputModel { } evt['type'] = type; - if (type == 'down' && !_pointerMovedAfterEnter) { + if (type == kMouseEventTypeDown && !_pointerMovedAfterEnter) { // Move mouse to the position of the down event first. lastMousePos = ui.Offset(x, y); refreshMousePos(); @@ -1372,6 +1372,14 @@ class InputModel { return null; } + return InputModel.getPointInRemoteRect( + true, peerPlatform, kind, evtType, evtX, evtY, rect, + buttons: buttons); + } + + static Point? getPointInRemoteRect(bool isLocalDesktop, String? peerPlatform, + String kind, String evtType, int evtX, int evtY, Rect rect, + {int buttons = kPrimaryMouseButton}) { int minX = rect.left.toInt(); // https://github.com/rustdesk/rustdesk/issues/6678 // For Windows, [0,maxX], [0,maxY] should be set to enable window snapping. @@ -1380,15 +1388,34 @@ class InputModel { int minY = rect.top.toInt(); int maxY = (rect.top + rect.height).toInt() - (peerPlatform == kPeerPlatformWindows ? 0 : 1); - evtX = trySetNearestRange(evtX, minX, maxX, 5); - evtY = trySetNearestRange(evtY, minY, maxY, 5); - if (kind == kPointerEventKindMouse) { - if (evtX < minX || evtY < minY || evtX > maxX || evtY > maxY) { - // If left mouse up, no early return. - if (!(buttons == kPrimaryMouseButton && evtType == 'up')) { - return null; + evtX = InputModel.tryGetNearestRange(evtX, minX, maxX, 5); + evtY = InputModel.tryGetNearestRange(evtY, minY, maxY, 5); + if (isLocalDesktop) { + if (kind == kPointerEventKindMouse) { + if (evtX < minX || evtY < minY || evtX > maxX || evtY > maxY) { + // If left mouse up, no early return. + if (!(buttons == kPrimaryMouseButton && + evtType == kMouseEventTypeUp)) { + return null; + } } } + } else { + bool evtXInRange = evtX >= minX && evtX <= maxX; + bool evtYInRange = evtY >= minY && evtY <= maxY; + if (!(evtXInRange || evtYInRange)) { + return null; + } + if (evtX < minX) { + evtX = minX; + } else if (evtX > maxX) { + evtX = maxX; + } + if (evtY < minY) { + evtY = minY; + } else if (evtY > maxY) { + evtY = maxY; + } } return Point(evtX, evtY); diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 80a6809d5181..7765ebc40867 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -6,6 +6,7 @@ import 'dart:ui' as ui; import 'package:bot_toast/bot_toast.dart'; import 'package:desktop_multi_window/desktop_multi_window.dart'; +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_hbb/common/widgets/peers_view.dart'; @@ -1516,10 +1517,13 @@ class CanvasModel with ChangeNotifier { _x = (size.width - displayWidth * _scale) / 2; _y = (size.height - displayHeight * _scale) / 2; _imageOverflow.value = _x < 0 || y < 0; + if (isMobile && style == kRemoteViewStyleOriginal) { + _moveToCenterCursor(); + } if (notify) { notifyListeners(); } - if (refreshMousePos) { + if (!isMobile && refreshMousePos) { parent.target?.inputModel.refreshMousePos(); } tryUpdateScrollStyle(Duration.zero, style); @@ -1709,8 +1713,6 @@ class CanvasModel with ChangeNotifier { _timerMobileFocusCanvasCursor = Timer(Duration(milliseconds: 100), () async { await updateViewStyle(refreshMousePos: false, notify: false); - _moveToCenterCursor(); - parent.target?.cursorModel.ensureCursorInVisibleRect(); notifyListeners(); }); } @@ -2015,17 +2017,6 @@ class CursorModel with ChangeNotifier { return Offset(xoffset, yoffset); } - void ensureCursorInVisibleRect() { - final ensureVisibleValue = 50.0; - final r = getVisibleRect(); - final minX = r.left; - final maxX = max(r.right - ensureVisibleValue, r.left); - final minY = r.top; - final maxY = max(r.bottom - ensureVisibleValue, minY); - _x = min(max(_x, minX), maxX); - _y = min(max(_y, minY), maxY); - } - get scale => parent.target?.canvasModel.scale ?? 1.0; // mobile Soft keyboard, block touch event from the KeyHelpTools @@ -2042,6 +2033,7 @@ class CursorModel with ChangeNotifier { return false; } + // For touch mode move(double x, double y) { if (shouldBlock(x, y)) { _lastIsBlocked = true; @@ -2129,13 +2121,34 @@ class CursorModel with ChangeNotifier { } if (dx == 0 && dy == 0) return; - _x += dx; - _y += dy; + + Point? newPos; + final rect = parent.target?.ffiModel.rect; + if (rect == null) { + // unreachable + return; + } + newPos = InputModel.getPointInRemoteRect( + false, + parent.target?.ffiModel.pi.platform, + kPointerEventKindMouse, + kMouseEventTypeDefault, + (_x + dx).toInt(), + (_y + dy).toInt(), + rect, + buttons: kPrimaryButton); + if (newPos == null) { + return; + } + dx = newPos.x - _x; + dy = newPos.y - _y; + _x = newPos.x.toDouble(); + _y = newPos.y.toDouble(); if (tryMoveCanvasX && dx != 0) { - parent.target?.canvasModel.panX(-dx); + parent.target?.canvasModel.panX(-dx * scale); } if (tryMoveCanvasY && dy != 0) { - parent.target?.canvasModel.panY(-dy); + parent.target?.canvasModel.panY(-dy * scale); } parent.target?.inputModel.moveMouse(_x, _y); From a4bd23c9de3ffa9538180bc24fcb8dff687786fc Mon Sep 17 00:00:00 2001 From: 21pages Date: Tue, 5 Nov 2024 11:28:27 +0800 Subject: [PATCH 351/541] fix missing window focus service on windows/macos (#9824) Signed-off-by: 21pages --- src/server.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/server.rs b/src/server.rs index 02522db96841..eca20b829bba 100644 --- a/src/server.rs +++ b/src/server.rs @@ -111,6 +111,8 @@ pub fn new() -> ServerPtr { // wayland does not support multiple displays currently server.add_service(Box::new(input_service::new_window_focus())); } + #[cfg(not(target_os = "linux"))] + server.add_service(Box::new(input_service::new_window_focus())); } } Arc::new(RwLock::new(server)) From 5cfd1701fb5fe701ec379c1ca91bb4c9ded019b5 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Tue, 5 Nov 2024 17:55:38 +0800 Subject: [PATCH 352/541] fix: mobile input, touch mode, in display (#9827) Signed-off-by: fufesou --- flutter/lib/common/widgets/remote_input.dart | 42 +++++++++- flutter/lib/models/model.dart | 88 ++++++++++++++++++-- 2 files changed, 120 insertions(+), 10 deletions(-) diff --git a/flutter/lib/common/widgets/remote_input.dart b/flutter/lib/common/widgets/remote_input.dart index bea1490fe5c5..c6fdaf75b84f 100644 --- a/flutter/lib/common/widgets/remote_input.dart +++ b/flutter/lib/common/widgets/remote_input.dart @@ -86,6 +86,12 @@ class _RawTouchGestureDetectorRegionState PointerDeviceKind? lastDeviceKind; + // For touch mode, onDoubleTap + // `onDoubleTap()` does not provide the position of the tap event. + Offset _lastPosOfDoubleTapDown = Offset.zero; + bool _touchModePanStarted = false; + Offset _doubleFinerTapPosition = Offset.zero; + FFI get ffi => widget.ffi; FfiModel get ffiModel => widget.ffiModel; InputModel get inputModel => widget.inputModel; @@ -106,6 +112,7 @@ class _RawTouchGestureDetectorRegionState return; } if (handleTouch) { + _lastPosOfDoubleTapDown = d.localPosition; // Desktop or mobile "Touch mode" if (ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy)) { inputModel.tapDown(MouseButtons.left); @@ -140,6 +147,7 @@ class _RawTouchGestureDetectorRegionState return; } if (handleTouch) { + _lastPosOfDoubleTapDown = d.localPosition; ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy); } } @@ -151,6 +159,10 @@ class _RawTouchGestureDetectorRegionState if (ffiModel.touchMode && ffi.cursorModel.lastIsBlocked) { return; } + if (handleTouch && + !ffi.cursorModel.isInRemoteRect(_lastPosOfDoubleTapDown)) { + return; + } inputModel.tap(MouseButtons.left); inputModel.tap(MouseButtons.left); } @@ -161,8 +173,11 @@ class _RawTouchGestureDetectorRegionState return; } if (handleTouch) { - ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy); + _lastPosOfDoubleTapDown = d.localPosition; _cacheLongPressPosition = d.localPosition; + if (!ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy)) { + return; + } _cacheLongPressPositionTs = DateTime.now().millisecondsSinceEpoch; } } @@ -182,8 +197,10 @@ class _RawTouchGestureDetectorRegionState return; } if (handleTouch) { - ffi.cursorModel - .move(_cacheLongPressPosition.dx, _cacheLongPressPosition.dy); + if (!ffi.cursorModel + .move(_cacheLongPressPosition.dx, _cacheLongPressPosition.dy)) { + return; + } } if (!ffi.ffiModel.isPeerMobile) { inputModel.tap(MouseButtons.right); @@ -195,6 +212,7 @@ class _RawTouchGestureDetectorRegionState if (lastDeviceKind != PointerDeviceKind.touch) { return; } + _doubleFinerTapPosition = d.localPosition; // ignore for desktop and mobile } @@ -203,7 +221,13 @@ class _RawTouchGestureDetectorRegionState if (lastDeviceKind != PointerDeviceKind.touch) { return; } - if ((isDesktop || isWebDesktop) || !ffiModel.touchMode) { + + // mobile mouse mode or desktop touch screen + final isMobileMouseMode = isMobile && !ffiModel.touchMode; + // We can't use `d.localPosition` here because it's always (0, 0) on desktop. + final isDesktopInRemoteRect = (isDesktop || isWebDesktop) && + ffi.cursorModel.isInRemoteRect(_doubleFinerTapPosition); + if (isMobileMouseMode || isDesktopInRemoteRect) { inputModel.tap(MouseButtons.right); } } @@ -245,9 +269,15 @@ class _RawTouchGestureDetectorRegionState if (ffi.cursorModel.shouldBlock(d.localPosition.dx, d.localPosition.dy)) { return; } + if (!ffi.cursorModel.isInRemoteRect(d.localPosition)) { + return; + } + + _touchModePanStarted = true; 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. @@ -280,10 +310,14 @@ class _RawTouchGestureDetectorRegionState if (ffi.cursorModel.shouldBlock(d.localPosition.dx, d.localPosition.dy)) { return; } + if (handleTouch && !_touchModePanStarted) { + return; + } ffi.cursorModel.updatePan(d.delta, d.localPosition, handleTouch); } onOneFingerPanEnd(DragEndDetails d) { + _touchModePanStarted = false; if (lastDeviceKind != PointerDeviceKind.touch) { return; } diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 7765ebc40867..616d80d11435 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -9,6 +9,7 @@ import 'package:desktop_multi_window/desktop_multi_window.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; import 'package:flutter_hbb/common/widgets/peers_view.dart'; import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/models/ab_model.dart'; @@ -2040,16 +2041,56 @@ class CursorModel with ChangeNotifier { return false; } _lastIsBlocked = false; - moveLocal(x, y, adjust: parent.target?.canvasModel.getAdjustY() ?? 0); + if (!_moveLocalIfInRemoteRect(x, y)) { + return false; + } parent.target?.inputModel.moveMouse(_x, _y); return true; } - moveLocal(double x, double y, {double adjust = 0}) { + bool isInRemoteRect(Offset offset) { + return getRemotePosInRect(offset) != null; + } + + Offset? getRemotePosInRect(Offset offset) { + final adjust = parent.target?.canvasModel.getAdjustY() ?? 0; + final newPos = _getNewPos(offset.dx, offset.dy, adjust); + final visibleRect = getVisibleRect(); + if (!isPointInRect(newPos, visibleRect)) { + return null; + } + final rect = parent.target?.ffiModel.rect; + if (rect != null) { + if (!isPointInRect(newPos, rect)) { + return null; + } + } + return newPos; + } + + Offset _getNewPos(double x, double y, double adjust) { final xoffset = parent.target?.canvasModel.x ?? 0; final yoffset = parent.target?.canvasModel.y ?? 0; - _x = (x - xoffset) / scale + _displayOriginX; - _y = (y - yoffset - adjust) / scale + _displayOriginY; + final newX = (x - xoffset) / scale + _displayOriginX; + final newY = (y - yoffset - adjust) / scale + _displayOriginY; + return Offset(newX, newY); + } + + bool _moveLocalIfInRemoteRect(double x, double y) { + final newPos = getRemotePosInRect(Offset(x, y)); + if (newPos == null) { + return false; + } + _x = newPos.dx; + _y = newPos.dy; + notifyListeners(); + return true; + } + + moveLocal(double x, double y, {double adjust = 0}) { + final newPos = _getNewPos(x, y, adjust); + _x = newPos.dx; + _y = newPos.dy; notifyListeners(); } @@ -2182,9 +2223,44 @@ class CursorModel with ChangeNotifier { } } if (!isMoved) { + final rect = parent.target?.ffiModel.rect; + if (rect == null) { + // unreachable + return; + } + + Offset? movementInRect(double x, double y, Rect r) { + final isXInRect = x >= r.left && x <= r.right; + final isYInRect = y >= r.top && y <= r.bottom; + if (!(isXInRect || isYInRect)) { + return null; + } + if (x < r.left) { + x = r.left; + } else if (x > r.right) { + x = r.right; + } + if (y < r.top) { + y = r.top; + } else if (y > r.bottom) { + y = r.bottom; + } + return Offset(x, y); + } + final scale = parent.target?.canvasModel.scale ?? 1.0; - _x += delta.dx / scale; - _y += delta.dy / scale; + var movement = + movementInRect(_x + delta.dx / scale, _y + delta.dy / scale, rect); + if (movement == null) { + return; + } + movement = movementInRect(movement.dx, movement.dy, getVisibleRect()); + if (movement == null) { + return; + } + + _x = movement.dx; + _y = movement.dy; parent.target?.inputModel.moveMouse(_x, _y); } notifyListeners(); From 78088360ca7dd8de777094d56a3511e12251d9e7 Mon Sep 17 00:00:00 2001 From: notlin4 <121224522+notlin4@users.noreply.github.com> Date: Wed, 6 Nov 2024 07:59:12 +0800 Subject: [PATCH 353/541] Fix traditional Chinese localization (#9833) --- src/lang/tw.rs | 110 ++++++++++++++++++++++++------------------------- 1 file changed, 55 insertions(+), 55 deletions(-) diff --git a/src/lang/tw.rs b/src/lang/tw.rs index b0f64f82d397..8c644d6cfea2 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -7,7 +7,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Password", "密碼"), ("Ready", "就緒"), ("Established", "已建立"), - ("connecting_status", "正在連線到 RustDesk 網路 ..."), + ("connecting_status", "正在連線到 RustDesk 網路..."), ("Enable service", "啟用服務"), ("Start service", "啟動服務"), ("Service is running", "服務正在執行"), @@ -45,7 +45,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Website", "網站"), ("About", "關於"), ("Slogan_tip", "在這個混沌的世界中用心製作!"), - ("Privacy Statement", "隱私權聲明"), + ("Privacy Statement", "隱私權宣告"), ("Mute", "靜音"), ("Build Date", "建構日期"), ("Version", "版本"), @@ -76,12 +76,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Connection Error", "連線錯誤"), ("Error", "錯誤"), ("Reset by the peer", "對方重設了連線"), - ("Connecting...", "正在連線 ..."), + ("Connecting...", "正在連線..."), ("Connection in progress. Please wait.", "正在連線,請稍候。"), ("Please try 1 minute later", "請於 1 分鐘後再試"), ("Login Error", "登入錯誤"), ("Successful", "成功"), - ("Connected, waiting for image...", "已連線,等待畫面傳輸 ..."), + ("Connected, waiting for image...", "已連線,等待畫面傳輸..."), ("Name", "名稱"), ("Type", "類型"), ("Modified", "修改時間"), @@ -107,9 +107,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Are you sure you want to delete the file of this directory?", "您確定要刪除此資料夾中的檔案嗎?"), ("Do this for all conflicts", "套用到其他衝突"), ("This is irreversible!", "此操作不可逆!"), - ("Deleting", "正在刪除 ..."), + ("Deleting", "正在刪除..."), ("files", "檔案"), - ("Waiting", "正在等候 ..."), + ("Waiting", "正在等候..."), ("Finished", "已完成"), ("Speed", "速度"), ("Custom Image Quality", "自訂畫面品質"), @@ -120,8 +120,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Original", "原始"), ("Shrink", "縮減"), ("Stretch", "延展"), - ("Scrollbar", "滾動條"), - ("ScrollAuto", "自動滾動"), + ("Scrollbar", "捲動條"), + ("ScrollAuto", "自動捲動"), ("Good image quality", "最佳化畫面品質"), ("Balanced", "平衡"), ("Optimize reaction time", "最佳化反應時間"), @@ -150,9 +150,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Click to download", "點選以下載"), ("Click to update", "點選以更新"), ("Configure", "設定"), - ("config_acc", "為了遠端控制您的桌面,您需要授予 RustDesk「協助工具」權限。"), + ("config_acc", "為了遠端控制您的桌面,您需要授予 RustDesk「無障礙功能」權限。"), ("config_screen", "為了遠端存取您的桌面,您需要授予 RustDesk「螢幕錄製」權限。"), - ("Installing ...", "正在安裝 ..."), + ("Installing ...", "正在安裝..."), ("Install", "安裝"), ("Installation", "安裝"), ("Installation Path", "安裝路徑"), @@ -161,16 +161,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("agreement_tip", "開始安裝即表示您接受授權條款。"), ("Accept and Install", "接受並安裝"), ("End-user license agreement", "終端使用者授權合約"), - ("Generating ...", "正在產生 ..."), + ("Generating ...", "正在產生..."), ("Your installation is lower version.", "您安裝的版本過舊。"), ("not_close_tcp_tip", "在使用通道時請不要關閉此視窗"), - ("Listening ...", "正在等待通道連線 ..."), + ("Listening ...", "正在等待通道連線..."), ("Remote Host", "遠端主機"), ("Remote Port", "遠端連接埠"), ("Action", "操作"), ("Add", "新增"), ("Local Port", "本機連接埠"), - ("Local Address", "本機地址"), + ("Local Address", "本機位址"), ("Change Local Port", "修改本機連接埠"), ("setup_server_tip", "若您需要更快的連線速度,您可以選擇自行建立伺服器"), ("Too short, at least 6 characters.", "過短,至少需要 6 個字元。"), @@ -187,8 +187,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Relayed and unencrypted connection", "中繼且未加密的連線"), ("Enter Remote ID", "輸入遠端 ID"), ("Enter your password", "輸入您的密碼"), - ("Logging in...", "正在登入 ..."), - ("Enable RDP session sharing", "啟用 RDP 工作階段共享"), + ("Logging in...", "正在登入..."), + ("Enable RDP session sharing", "啟用 RDP 工作階段分享"), ("Auto Login", "自動登入 (只在您設定「工作階段結束後鎖定」時有效)"), ("Enable direct IP access", "啟用 IP 直接存取"), ("Rename", "重新命名"), @@ -216,9 +216,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Login", "登入"), ("Verify", "驗證"), ("Remember me", "記住我"), - ("Trust this device", "信任此裝置"), + ("Trust this device", "信任這部裝置"), ("Verification code", "驗證碼"), - ("verification_tip", "驗證碼已發送到註冊的電子郵件地址,請輸入驗證碼以繼續登入。"), + ("verification_tip", "驗證碼已傳送到註冊的電子郵件地址,請輸入驗證碼以繼續登入。"), ("Logout", "登出"), ("Tags", "標籤"), ("Search ID", "搜尋 ID"), @@ -329,7 +329,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Settings", "顯示設定"), ("Ratio", "比例"), ("Image Quality", "畫質"), - ("Scroll Style", "滾動樣式"), + ("Scroll Style", "捲動樣式"), ("Show Toolbar", "顯示工具列"), ("Hide Toolbar", "隱藏工具列"), ("Direct Connection", "直接連線"), @@ -354,7 +354,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Direct IP Access", "IP 直接連線"), ("Proxy", "代理伺服器"), ("Apply", "套用"), - ("Disconnect all devices?", "中斷所有遠端連線?"), + ("Disconnect all devices?", "是否中斷所有遠端連線?"), ("Clear", "清空"), ("Audio Input Device", "音訊輸入裝置"), ("Use IP Whitelisting", "只允許白名單上的 IP 進行連線"), @@ -373,7 +373,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Deny LAN discovery", "拒絕區域網路探索"), ("Write a message", "輸入聊天訊息"), ("Prompt", "提示"), - ("Please wait for confirmation of UAC...", "請等待對方確認 UAC ..."), + ("Please wait for confirmation of UAC...", "請等待對方確認 UAC..."), ("elevated_foreground_window_tip", "目前遠端桌面的視窗需要更高的權限才能繼續操作,您暫時無法使用滑鼠和鍵盤,您可以請求對方最小化目前視窗,或者在連線管理視窗點選提升權限。為了避免這個問題,建議在遠端裝置上安裝本軟體。"), ("Disconnected", "斷開連線"), ("Other", "其他"), @@ -394,7 +394,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Accept sessions via password", "只允許透過輸入密碼進行連線"), ("Accept sessions via click", "只允許透過點選接受進行連線"), ("Accept sessions via both", "允許輸入密碼或點選接受進行連線"), - ("Please wait for the remote side to accept your session request...", "請等待對方接受您的連線請求 ..."), + ("Please wait for the remote side to accept your session request...", "請等待對方接受您的連線請求..."), ("One-time Password", "一次性密碼"), ("Use one-time password", "使用一次性密碼"), ("One-time password length", "一次性密碼長度"), @@ -410,16 +410,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by web console", "被 Web 控制台手動關閉"), ("Local keyboard type", "本機鍵盤類型"), ("Select local keyboard type", "請選擇本機鍵盤類型"), - ("software_render_tip", "如果您使用 Nvidia 顯示卡,並且遠端視窗在建立連線後會立刻關閉,那麼請安裝 nouveau 顯示卡驅動程式並且選擇使用軟體渲染可能會有幫助。重新啟動軟體後生效。"), - ("Always use software rendering", "使用軟體渲染"), - ("config_input", "為了能夠透過鍵盤控制遠端桌面,請給予 RustDesk \"輸入監控\" 權限。"), - ("config_microphone", "為了支援透過麥克風進行音訊傳輸,請給予 RustDesk \"錄音\"權限。"), + ("software_render_tip", "如果您使用 Nvidia 顯示卡,並且遠端視窗在建立連線後會立刻關閉,那麼請安裝 nouveau 顯示卡驅動程式並且選擇使用軟體繪製可能會有幫助。重新啟動軟體後生效。"), + ("Always use software rendering", "使用軟體繪製"), + ("config_input", "為了能夠透過鍵盤控制遠端桌面,請給予 RustDesk「輸入監控」權限。"), + ("config_microphone", "為了支援透過麥克風進行音訊傳輸,請給予 RustDesk「錄音」權限。"), ("request_elevation_tip", "如果遠端使用者可以操作電腦,您可以請求提升權限。"), ("Wait", "等待"), ("Elevation Error", "權限提升失敗"), - ("Ask the remote user for authentication", "請求遠端使用者進行身分驗證"), + ("Ask the remote user for authentication", "請求遠端使用者進行驗證驗證"), ("Choose this if the remote account is administrator", "當遠端使用者帳戶是管理員時,請選擇此選項"), - ("Transmit the username and password of administrator", "發送管理員的使用者名稱和密碼"), + ("Transmit the username and password of administrator", "傳送管理員的使用者名稱和密碼"), ("still_click_uac_tip", "依然需要遠端使用者在執行 RustDesk 時於 UAC 視窗點選「是」。"), ("Request Elevation", "請求權限提升"), ("wait_accept_uac_tip", "請等待遠端使用者確認 UAC 對話框。"), @@ -436,17 +436,17 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please confirm if you want to share your desktop?", "請確認是否要讓對方存取您的桌面?"), ("Display", "顯示"), ("Default View Style", "預設顯示方式"), - ("Default Scroll Style", "預設滾動方式"), - ("Default Image Quality", "預設圖像品質"), + ("Default Scroll Style", "預設捲動方式"), + ("Default Image Quality", "預設影像品質"), ("Default Codec", "預設編解碼器"), ("Bitrate", "位元速率"), - ("FPS", "幀率"), + ("FPS", "FPS"), ("Auto", "自動"), ("Other Default Options", "其他預設選項"), ("Voice call", "語音通話"), ("Text chat", "文字聊天"), ("Stop voice call", "停止語音通話"), - ("relay_hint_tip", "可能無法使用直接連線,您可以嘗試中繼連線。\n另外,如果想要直接使用中繼連線,您可以在 ID 後面新增 \"/r\",或是如果近期的工作階段裡存在該設備,您也可以在設備選項裡選擇「一律透過中繼連線」。"), + ("relay_hint_tip", "可能無法使用直接連線,您可以嘗試中繼連線。\n另外,如果想要直接使用中繼連線,您可以在 ID 後面新增「/r」,或是如果近期的工作階段裡存在該裝置,您也可以在裝置選項裡選擇「一律透過中繼連線」。"), ("Reconnect", "重新連線"), ("Codec", "編解碼器"), ("Resolution", "解析度"), @@ -527,7 +527,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Toggle Tags", "切換標籤"), ("pull_ab_failed_tip", "通訊錄更新失敗"), ("push_ab_failed_tip", "成功同步通訊錄至伺服器"), - ("synced_peer_readded_tip", "最近會話中存在的設備將會被重新同步到通訊錄。"), + ("synced_peer_readded_tip", "最近工作階段中存在的裝置將會被重新同步到通訊錄。"), ("Change Color", "更改顏色"), ("Primary Color", "基本色"), ("HSV Color", "HSV 色"), @@ -559,17 +559,17 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change view", "更改檢視方式"), ("Big tiles", "大磁磚"), ("Small tiles", "小磁磚"), - ("List", "列表"), + ("List", "清單"), ("Virtual display", "虛擬螢幕"), ("Plug out all", "拔出所有"), ("True color (4:4:4)", "全彩模式(4:4:4)"), ("Enable blocking user input", "允許封鎖使用者輸入"), - ("id_input_tip", "您可以輸入 ID、IP、或網域名稱+通訊埠號(<網域名稱>:<通訊埠號>)。\n如果您要存取位於其他伺服器上的設備,請在 ID 之後添加伺服器地址(@<伺服器地址>?key=<金鑰>)\n例如:9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=\n要存取公共伺服器上的設備,請輸入\"@public\",不需輸入金鑰。\n\n如果您想要在第一次連線時,強制使用中繼連接,請在 ID 的末尾添加 \"/r\",例如,\"9123456234/r\"。"), + ("id_input_tip", "您可以輸入 ID、IP、或網域名稱+連接埠(<網域名稱>:<連接埠>)。\n如果您要存取位於其他伺服器上的裝置,請在 ID 之後新增伺服器位址(@<伺服器位址>?key=<金鑰>)\n例如:9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=\n要存取公共伺服器上的裝置,請輸入「@public」,不需輸入金鑰。\n\n如果您想要在第一次連線時,強制使用中繼連線,請在 ID 的結尾新增「/r」,例如,「9123456234/r」。"), ("privacy_mode_impl_mag_tip", "模式 1"), ("privacy_mode_impl_virtual_display_tip", "模式 2"), ("Enter privacy mode", "進入隱私模式"), ("Exit privacy mode", "退出隱私模式"), - ("idd_not_support_under_win10_2004_tip", "不支援 Indirect display driver。 需要 Windows 10 版本 2004 或更新的版本。"), + ("idd_not_support_under_win10_2004_tip", "不支援 Indirect display driver。需要 Windows 10 版本 2004 以上版本。"), ("input_source_1_tip", "輸入源 1"), ("input_source_2_tip", "輸入源 2"), ("Swap control-command key", "交換 Control 和 Command 按鍵"), @@ -577,28 +577,28 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("2FA code", "二步驟驗證碼"), ("More", "更多"), ("enable-2fa-title", "啟用二步驟驗證"), - ("enable-2fa-desc", "現在請您設定您的二步驟驗證程式。 您可以在手機或電腦使用 Authy、Microsoft 或 Google Authenticator 等驗證器程式。\n\n用它掃描QR Code或輸入下方金鑰至您的驗證器,然後輸入顯示的驗證碼以啟用二步驟驗證。"), - ("wrong-2fa-code", "無法驗證此驗證碼。 請確認您的驗證碼和您的本地時間設置是正確的"), + ("enable-2fa-desc", "現在請您設定您的二步驟驗證程式。您可以在手機或電腦使用 Authy、Microsoft 或 Google Authenticator 等驗證器程式。\n\n用它掃描QR Code或輸入下方金鑰至您的驗證器,然後輸入顯示的驗證碼以啟用二步驟驗證。"), + ("wrong-2fa-code", "無法驗證此驗證碼。請確認您的驗證碼和您的本機時間設定是正確的"), ("enter-2fa-title", "二步驟驗證"), - ("Email verification code must be 6 characters.", "Email 驗證碼必須是 6 個字元。"), + ("Email verification code must be 6 characters.", "電子郵件驗證碼必須是 6 個字元。"), ("2FA code must be 6 digits.", "二步驟驗證碼必須是 6 位數字。"), ("Multiple Windows sessions found", "發現多個 Windows 工作階段"), ("Please select the session you want to connect to", "請選擇您想要連結的工作階段"), ("powered_by_me", "由 RustDesk 提供支援"), - ("outgoing_only_desk_tip", "目前版本的軟體是自定義版本。\n您可以連接至其他設備,但是其他設備無法連接至您的設備。"), - ("preset_password_warning", "此客製化版本附有預設密碼。任何知曉此密碼的人都能完全控制您的裝置。如果這不是您所預期的,請立即卸載此軟體。"), + ("outgoing_only_desk_tip", "目前版本的軟體是自訂版本。\n您可以連線至其他裝置,但是其他裝置無法連線至您的裝置。"), + ("preset_password_warning", "此客製化版本附有預設密碼。任何知曉此密碼的人都能完全控制您的裝置。如果這不是您所預期的,請立即移除此軟體。"), ("Security Alert", "安全警告"), ("My address book", "我的通訊錄"), ("Personal", "個人的"), ("Owner", "擁有者"), - ("Set shared password", "設定共享密碼"), + ("Set shared password", "設定共用密碼"), ("Exist in", "存在於"), ("Read-only", "唯讀"), ("Read/Write", "讀寫"), ("Full Control", "完全控制"), - ("share_warning_tip", "上述的欄位為共享且對其他人可見。"), + ("share_warning_tip", "上述的欄位為共用且對其他人可見。"), ("Everyone", "所有人"), - ("ab_web_console_tip", "打開 Web 控制台以進行更多操作"), + ("ab_web_console_tip", "開啟 Web 控制台以進行更多操作"), ("allow-only-conn-window-open-tip", "只在 RustDesk 視窗開啟時允許連接"), ("no_need_privacy_mode_no_physical_displays_tip", "沒有物理螢幕,沒必要使用隱私模式。"), ("Follow remote cursor", "跟隨遠端游標"), @@ -611,8 +611,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("clear_Wayland_screen_selection_tip", "清除 Wayland 的螢幕選擇後,您可以重新選擇分享的螢幕。"), ("confirm_clear_Wayland_screen_selection_tip", "是否確認清除 Wayland 的分享螢幕選擇?"), ("android_new_voice_call_tip", "收到新的語音通話請求。如果您接受,音訊將切換為語音通訊。"), - ("texture_render_tip", "使用紋理渲染,讓圖片更加順暢。 如果您遭遇渲染問題,可嘗試關閉此選項。"), - ("Use texture rendering", "使用紋理渲染"), + ("texture_render_tip", "使用紋理繪製,讓圖片更加順暢。如果您遭遇繪製問題,可嘗試關閉此選項。"), + ("Use texture rendering", "使用紋理繪製"), ("Floating window", "懸浮視窗"), ("floating_window_tip", "有助於保持 RustDesk 後台服務"), ("Keep screen on", "保持螢幕開啟"), @@ -627,28 +627,28 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Power", "電源"), ("Telegram bot", "Telegram 機器人"), ("enable-bot-tip", "如果您啟用此功能,您可以從您的機器人接收二步驟驗證碼,亦可作為連線通知之用。"), - ("enable-bot-desc", "1. 開啟與 @BotFather 的對話。\n2. 傳送指令 \"/newbot\"。 您將會在完成此步驟後收到權杖 (Token)。\n3. 開始與您剛創立的機器人的對話。 傳送一則以正斜槓 (\"/\") 開頭的訊息來啟用它,例如 \"/hello\"。"), + ("enable-bot-desc", "1. 開啟與 @BotFather 的對話。\n2. 傳送指令「/newbot」。您將會在完成此步驟後收到權杖 (Token)。\n3. 開始與您剛創立的機器人的對話。傳送一則以正斜線 (「/」) 開頭的訊息來啟用它,例如「/hello」。"), ("cancel-2fa-confirm-tip", "確定要取消二步驟驗證嗎?"), ("cancel-bot-confirm-tip", "確定要取消 Telegram 機器人嗎?"), ("About RustDesk", "關於 RustDesk"), - ("Send clipboard keystrokes", "發送剪貼簿按鍵"), - ("network_error_tip", "請檢查網路連結,然後點擊重試"), + ("Send clipboard keystrokes", "傳送剪貼簿按鍵"), + ("network_error_tip", "請檢查網路連結,然後點選重試"), ("Unlock with PIN", "使用 PIN 碼解鎖設定"), ("Requires at least {} characters", "不少於 {} 個字元"), ("Wrong PIN", "PIN 碼錯誤"), ("Set PIN", "設定 PIN 碼"), - ("Enable trusted devices", "啟用信任設備"), - ("Manage trusted devices", "管理信任設備"), + ("Enable trusted devices", "啟用信任裝置"), + ("Manage trusted devices", "管理信任裝置"), ("Platform", "平台"), ("Days remaining", "剩餘天數"), - ("enable-trusted-devices-tip", "允許受信任的設備跳過 2FA 驗證"), + ("enable-trusted-devices-tip", "允許受信任的裝置跳過 2FA 驗證"), ("Parent directory", "父目錄"), ("Resume", "繼續"), - ("Invalid file name", "無效文件名"), - ("one-way-file-transfer-tip", "被控端啟用了單向文件傳輸"), - ("Authentication Required", "需要身分驗證"), + ("Invalid file name", "無效檔名"), + ("one-way-file-transfer-tip", "被控端啟用了單向檔案傳輸"), + ("Authentication Required", "需要驗證驗證"), ("Authenticate", "認證"), - ("web_id_input_tip", "您可以輸入同一個伺服器內的 ID,Web 客戶端不支援直接 IP 存取。\n如果您要存取位於其他伺服器上的設備,請在 ID 之後添加伺服器地址(@<伺服器地址>?key=<金鑰>)\n例如:9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=\n要存取公共伺服器上的設備,請輸入\"@public\",不需輸入金鑰。"), + ("web_id_input_tip", "您可以輸入同一個伺服器內的 ID,Web 用戶端不支援直接 IP 存取。\n如果您要存取位於其他伺服器上的裝置,請在 ID 之後新增伺服器位址(@<伺服器位址>?key=<金鑰>)\n例如:9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=\n要存取公共伺服器上的裝置,請輸入「@public」,不需輸入金鑰。"), ("Download", "下載"), ("Upload folder", "上傳資料夾"), ("Upload files", "上傳檔案"), From faf97c770cc5a3a2c65a9b9734c37f57ea493e44 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Wed, 6 Nov 2024 21:59:23 +0800 Subject: [PATCH 354/541] fix: mobile, cursor mode, don't reset canvas (#9843) Signed-off-by: fufesou --- flutter/lib/mobile/pages/remote_page.dart | 18 +++++++++---- flutter/lib/models/model.dart | 32 +++++++++++------------ 2 files changed, 29 insertions(+), 21 deletions(-) diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index bdee525fda33..a1be9c009b95 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -542,7 +542,9 @@ class _RemotePageState extends State with WidgetsBindingObserver { right: 10, child: QualityMonitor(gFFI.qualityMonitorModel), ), - KeyHelpTools(requestShow: (keyboardIsVisible || _showGestureHelp)), + KeyHelpTools( + keyboardIsVisible: keyboardIsVisible, + showGestureHelp: _showGestureHelp), SizedBox( width: 0, height: 0, @@ -771,10 +773,14 @@ class _RemotePageState extends State with WidgetsBindingObserver { } class KeyHelpTools extends StatefulWidget { + final bool keyboardIsVisible; + final bool showGestureHelp; + /// need to show by external request, etc [keyboardIsVisible] or [changeTouchMode] - final bool requestShow; + bool get requestShow => keyboardIsVisible || showGestureHelp; - KeyHelpTools({required this.requestShow}); + KeyHelpTools( + {required this.keyboardIsVisible, required this.showGestureHelp}); @override State createState() => _KeyHelpToolsState(); @@ -819,7 +825,8 @@ class _KeyHelpToolsState extends State { final size = renderObject.size; Offset pos = renderObject.localToGlobal(Offset.zero); gFFI.cursorModel.keyHelpToolsVisibilityChanged( - Rect.fromLTWH(pos.dx, pos.dy, size.width, size.height)); + Rect.fromLTWH(pos.dx, pos.dy, size.width, size.height), + widget.keyboardIsVisible); } } @@ -831,7 +838,8 @@ class _KeyHelpToolsState extends State { inputModel.command; if (!_pin && !hasModifierOn && !widget.requestShow) { - gFFI.cursorModel.keyHelpToolsVisibilityChanged(null); + gFFI.cursorModel + .keyHelpToolsVisibilityChanged(null, widget.keyboardIsVisible); return Offstage(); } final size = MediaQuery.of(context).size; diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 616d80d11435..2ddbcd22e9c4 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -1515,12 +1515,8 @@ class CanvasModel with ChangeNotifier { if (kIgnoreDpi && style == kRemoteViewStyleOriginal) { _scale = 1.0 / _devicePixelRatio; } - _x = (size.width - displayWidth * _scale) / 2; - _y = (size.height - displayHeight * _scale) / 2; + _resetCanvasOffset(displayWidth, displayHeight); _imageOverflow.value = _x < 0 || y < 0; - if (isMobile && style == kRemoteViewStyleOriginal) { - _moveToCenterCursor(); - } if (notify) { notifyListeners(); } @@ -1530,6 +1526,14 @@ class CanvasModel with ChangeNotifier { tryUpdateScrollStyle(Duration.zero, style); } + _resetCanvasOffset(int displayWidth, int displayHeight) { + _x = (size.width - displayWidth * _scale) / 2; + _y = (size.height - displayHeight * _scale) / 2; + if (isMobile && _lastViewStyle.style == kRemoteViewStyleOriginal) { + _moveToCenterCursor(); + } + } + tryUpdateScrollStyle(Duration duration, String? style) async { if (_scrollStyle != ScrollStyle.scrollbar) return; style ??= await bind.sessionGetViewStyle(sessionId: sessionId); @@ -1640,8 +1644,7 @@ class CanvasModel with ChangeNotifier { if (isWebDesktop) { updateViewStyle(); } else { - _x = (size.width - getDisplayWidth() * _scale) / 2; - _y = (size.height - getDisplayHeight() * _scale) / 2; + _resetCanvasOffset(getDisplayWidth(), getDisplayHeight()); } notifyListeners(); } @@ -1677,10 +1680,7 @@ class CanvasModel with ChangeNotifier { if (kIgnoreDpi && _lastViewStyle.style == kRemoteViewStyleOriginal) { _scale = 1.0 / _devicePixelRatio; } - final displayWidth = getDisplayWidth(); - final displayHeight = getDisplayHeight(); - _x = (size.width - displayWidth * _scale) / 2; - _y = (size.height - displayHeight * _scale) / 2; + _resetCanvasOffset(getDisplayWidth(), getDisplayHeight()); bind.sessionSetViewStyle(sessionId: sessionId, value: _lastViewStyle.style); notifyListeners(); } @@ -1937,9 +1937,10 @@ class CursorModel with ChangeNotifier { // `lastIsBlocked` is only used in common/widgets/remote_input.dart -> _RawTouchGestureDetectorRegionState -> onDoubleTap() // Because onDoubleTap() doesn't have the `event` parameter, we can't get the touch event's position. bool _lastIsBlocked = false; + bool _lastKeyboardIsVisible = false; Rect? get keyHelpToolsRect => _keyHelpToolsRect; - keyHelpToolsVisibilityChanged(Rect? r) { + keyHelpToolsVisibilityChanged(Rect? r, bool keyboardIsVisible) { _keyHelpToolsRect = r; if (r == null) { _lastIsBlocked = false; @@ -1949,11 +1950,10 @@ class CursorModel with ChangeNotifier { // `lastIsBlocked` will be set when the cursor is moving or touch somewhere else. _lastIsBlocked = true; } - if (isMobile) { - if (r != null || _lastIsBlocked) { - parent.target?.canvasModel.mobileFocusCanvasCursor(); - } + if (isMobile && _lastKeyboardIsVisible != keyboardIsVisible) { + parent.target?.canvasModel.mobileFocusCanvasCursor(); } + _lastKeyboardIsVisible = keyboardIsVisible; } get lastIsBlocked => _lastIsBlocked; From 69277dd16b840bf4c61fc1b47eee049f3a4be212 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Thu, 7 Nov 2024 14:58:10 +0800 Subject: [PATCH 355/541] fix: mobile, don't adjust canvas on gesture help show up (#9846) Signed-off-by: fufesou --- flutter/lib/models/model.dart | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 2ddbcd22e9c4..f68801f583e4 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -1475,14 +1475,16 @@ class CanvasModel with ChangeNotifier { if (isMobile) { h = h - mediaData.viewInsets.bottom - - (parent.target?.cursorModel.keyHelpToolsRect?.bottom ?? 0); + (parent.target?.cursorModel.keyHelpToolsRectToAdjustCanvas?.bottom ?? + 0); } return Size(w < 0 ? 0 : w, h < 0 ? 0 : h); } // mobile only double getAdjustY() { - final bottom = parent.target?.cursorModel.keyHelpToolsRect?.bottom ?? 0; + final bottom = + parent.target?.cursorModel.keyHelpToolsRectToAdjustCanvas?.bottom ?? 0; return max(bottom - MediaQueryData.fromView(ui.window).padding.top, 0); } @@ -1939,7 +1941,8 @@ class CursorModel with ChangeNotifier { bool _lastIsBlocked = false; bool _lastKeyboardIsVisible = false; - Rect? get keyHelpToolsRect => _keyHelpToolsRect; + Rect? get keyHelpToolsRectToAdjustCanvas => + _lastKeyboardIsVisible ? _keyHelpToolsRect : null; keyHelpToolsVisibilityChanged(Rect? r, bool keyboardIsVisible) { _keyHelpToolsRect = r; if (r == null) { From d0ef52e4187dd20f178707c5977d16086f7d8215 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Thu, 7 Nov 2024 21:23:41 +0800 Subject: [PATCH 356/541] fix: touch input, ensure message orders (#9855) Signed-off-by: fufesou --- flutter/lib/common/widgets/remote_input.dart | 93 +++++++++++--------- flutter/lib/models/input_model.dart | 26 +++--- flutter/lib/models/model.dart | 14 +-- 3 files changed, 70 insertions(+), 63 deletions(-) diff --git a/flutter/lib/common/widgets/remote_input.dart b/flutter/lib/common/widgets/remote_input.dart index c6fdaf75b84f..fa635222d47b 100644 --- a/flutter/lib/common/widgets/remote_input.dart +++ b/flutter/lib/common/widgets/remote_input.dart @@ -106,7 +106,7 @@ class _RawTouchGestureDetectorRegionState ); } - onTapDown(TapDownDetails d) { + onTapDown(TapDownDetails d) async { lastDeviceKind = d.kind; if (lastDeviceKind != PointerDeviceKind.touch) { return; @@ -114,45 +114,49 @@ class _RawTouchGestureDetectorRegionState if (handleTouch) { _lastPosOfDoubleTapDown = d.localPosition; // Desktop or mobile "Touch mode" - if (ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy)) { - inputModel.tapDown(MouseButtons.left); + final isMoved = + await ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy); + if (isMoved) { + await inputModel.tapDown(MouseButtons.left); } } } - onTapUp(TapUpDetails d) { + onTapUp(TapUpDetails d) async { if (lastDeviceKind != PointerDeviceKind.touch) { return; } if (handleTouch) { - if (ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy)) { + final isMoved = + await ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy); + if (isMoved) { inputModel.tapUp(MouseButtons.left); } } } - onTap() { + onTap() async { if (lastDeviceKind != PointerDeviceKind.touch) { return; } if (!handleTouch) { // Mobile, "Mouse mode" - inputModel.tap(MouseButtons.left); + await inputModel.tap(MouseButtons.left); } } - onDoubleTapDown(TapDownDetails d) { + onDoubleTapDown(TapDownDetails d) async { lastDeviceKind = d.kind; if (lastDeviceKind != PointerDeviceKind.touch) { return; } if (handleTouch) { _lastPosOfDoubleTapDown = d.localPosition; - ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy); + await ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy); } } - onDoubleTap() { + onDoubleTap() async { if (lastDeviceKind != PointerDeviceKind.touch) { return; } @@ -163,11 +167,11 @@ class _RawTouchGestureDetectorRegionState !ffi.cursorModel.isInRemoteRect(_lastPosOfDoubleTapDown)) { return; } - inputModel.tap(MouseButtons.left); - inputModel.tap(MouseButtons.left); + await inputModel.tap(MouseButtons.left); + await inputModel.tap(MouseButtons.left); } - onLongPressDown(LongPressDownDetails d) { + onLongPressDown(LongPressDownDetails d) async { lastDeviceKind = d.kind; if (lastDeviceKind != PointerDeviceKind.touch) { return; @@ -175,39 +179,42 @@ class _RawTouchGestureDetectorRegionState if (handleTouch) { _lastPosOfDoubleTapDown = d.localPosition; _cacheLongPressPosition = d.localPosition; - if (!ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy)) { + final isMoved = + await ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy); + if (!isMoved) { return; } _cacheLongPressPositionTs = DateTime.now().millisecondsSinceEpoch; } } - onLongPressUp() { + onLongPressUp() async { if (lastDeviceKind != PointerDeviceKind.touch) { return; } if (handleTouch) { - inputModel.tapUp(MouseButtons.left); + await inputModel.tapUp(MouseButtons.left); } } // for mobiles - onLongPress() { + onLongPress() async { if (lastDeviceKind != PointerDeviceKind.touch) { return; } if (handleTouch) { - if (!ffi.cursorModel - .move(_cacheLongPressPosition.dx, _cacheLongPressPosition.dy)) { + final isMoved = await ffi.cursorModel + .move(_cacheLongPressPosition.dx, _cacheLongPressPosition.dy); + if (!isMoved) { return; } } if (!ffi.ffiModel.isPeerMobile) { - inputModel.tap(MouseButtons.right); + await inputModel.tap(MouseButtons.right); } } - onDoubleFinerTapDown(TapDownDetails d) { + onDoubleFinerTapDown(TapDownDetails d) async { lastDeviceKind = d.kind; if (lastDeviceKind != PointerDeviceKind.touch) { return; @@ -216,7 +223,7 @@ class _RawTouchGestureDetectorRegionState // ignore for desktop and mobile } - onDoubleFinerTap(TapDownDetails d) { + onDoubleFinerTap(TapDownDetails d) async { lastDeviceKind = d.kind; if (lastDeviceKind != PointerDeviceKind.touch) { return; @@ -228,39 +235,39 @@ class _RawTouchGestureDetectorRegionState final isDesktopInRemoteRect = (isDesktop || isWebDesktop) && ffi.cursorModel.isInRemoteRect(_doubleFinerTapPosition); if (isMobileMouseMode || isDesktopInRemoteRect) { - inputModel.tap(MouseButtons.right); + await inputModel.tap(MouseButtons.right); } } - onHoldDragStart(DragStartDetails d) { + onHoldDragStart(DragStartDetails d) async { lastDeviceKind = d.kind; if (lastDeviceKind != PointerDeviceKind.touch) { return; } if (!handleTouch) { - inputModel.sendMouse('down', MouseButtons.left); + await inputModel.sendMouse('down', MouseButtons.left); } } - onHoldDragUpdate(DragUpdateDetails d) { + onHoldDragUpdate(DragUpdateDetails d) async { if (lastDeviceKind != PointerDeviceKind.touch) { return; } if (!handleTouch) { - ffi.cursorModel.updatePan(d.delta, d.localPosition, handleTouch); + await ffi.cursorModel.updatePan(d.delta, d.localPosition, handleTouch); } } - onHoldDragEnd(DragEndDetails d) { + onHoldDragEnd(DragEndDetails d) async { if (lastDeviceKind != PointerDeviceKind.touch) { return; } if (!handleTouch) { - inputModel.sendMouse('up', MouseButtons.left); + await inputModel.sendMouse('up', MouseButtons.left); } } - onOneFingerPanStart(BuildContext context, DragStartDetails d) { + onOneFingerPanStart(BuildContext context, DragStartDetails d) async { lastDeviceKind = d.kind ?? lastDeviceKind; if (lastDeviceKind != PointerDeviceKind.touch) { return; @@ -285,11 +292,11 @@ class _RawTouchGestureDetectorRegionState // 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 + await ffi.cursorModel .move(_cacheLongPressPosition.dx, _cacheLongPressPosition.dy); } - inputModel.sendMouse('down', MouseButtons.left); - ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy); + await inputModel.sendMouse('down', MouseButtons.left); + await ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy); } else { final offset = ffi.cursorModel.offset; final cursorX = offset.dx; @@ -298,12 +305,12 @@ class _RawTouchGestureDetectorRegionState ffi.cursorModel.getVisibleRect().inflate(1); // extend edges final size = MediaQueryData.fromView(View.of(context)).size; if (!visible.contains(Offset(cursorX, cursorY))) { - ffi.cursorModel.move(size.width / 2, size.height / 2); + await ffi.cursorModel.move(size.width / 2, size.height / 2); } } } - onOneFingerPanUpdate(DragUpdateDetails d) { + onOneFingerPanUpdate(DragUpdateDetails d) async { if (lastDeviceKind != PointerDeviceKind.touch) { return; } @@ -313,10 +320,10 @@ class _RawTouchGestureDetectorRegionState if (handleTouch && !_touchModePanStarted) { return; } - ffi.cursorModel.updatePan(d.delta, d.localPosition, handleTouch); + await ffi.cursorModel.updatePan(d.delta, d.localPosition, handleTouch); } - onOneFingerPanEnd(DragEndDetails d) { + onOneFingerPanEnd(DragEndDetails d) async { _touchModePanStarted = false; if (lastDeviceKind != PointerDeviceKind.touch) { return; @@ -324,7 +331,7 @@ class _RawTouchGestureDetectorRegionState if (isDesktop || isWebDesktop) { ffi.cursorModel.clearRemoteWindowCoords(); } - inputModel.sendMouse('up', MouseButtons.left); + await inputModel.sendMouse('up', MouseButtons.left); } // scale + pan event @@ -334,7 +341,7 @@ class _RawTouchGestureDetectorRegionState } } - onTwoFingerScaleUpdate(ScaleUpdateDetails d) { + onTwoFingerScaleUpdate(ScaleUpdateDetails d) async { if (lastDeviceKind != PointerDeviceKind.touch) { return; } @@ -343,7 +350,7 @@ class _RawTouchGestureDetectorRegionState _scale = d.scale; if (scale != 0) { - bind.sessionSendPointer( + await bind.sessionSendPointer( sessionId: sessionId, msg: json.encode( PointerEventToRust(kPointerEventKindTouch, 'scale', scale) @@ -358,12 +365,12 @@ class _RawTouchGestureDetectorRegionState } } - onTwoFingerScaleEnd(ScaleEndDetails d) { + onTwoFingerScaleEnd(ScaleEndDetails d) async { if (lastDeviceKind != PointerDeviceKind.touch) { return; } if ((isDesktop || isWebDesktop)) { - bind.sessionSendPointer( + await bind.sessionSendPointer( sessionId: sessionId, msg: json.encode( PointerEventToRust(kPointerEventKindTouch, 'scale', 0).toJson())); @@ -373,7 +380,7 @@ class _RawTouchGestureDetectorRegionState // No idea why we need to set the view style to "" here. // bind.sessionSetViewStyle(sessionId: sessionId, value: ""); } - inputModel.sendMouse('up', MouseButtons.left); + await inputModel.sendMouse('up', MouseButtons.left); } get onHoldDragCancel => null; diff --git a/flutter/lib/models/input_model.dart b/flutter/lib/models/input_model.dart index 0cc0537d67bb..0692ba2df4d4 100644 --- a/flutter/lib/models/input_model.dart +++ b/flutter/lib/models/input_model.dart @@ -768,22 +768,22 @@ class InputModel { } /// Send a mouse tap event(down and up). - void tap(MouseButtons button) { - sendMouse('down', button); - sendMouse('up', button); + Future tap(MouseButtons button) async { + await sendMouse('down', button); + await sendMouse('up', button); } - void tapDown(MouseButtons button) { - sendMouse('down', button); + Future tapDown(MouseButtons button) async { + await sendMouse('down', button); } - void tapUp(MouseButtons button) { - sendMouse('up', button); + Future tapUp(MouseButtons button) async { + await sendMouse('up', button); } /// Send scroll event with scroll distance [y]. - void scroll(int y) { - bind.sessionSendMouse( + Future scroll(int y) async { + await bind.sessionSendMouse( sessionId: sessionId, msg: json .encode(modify({'id': id, 'type': 'wheel', 'y': y.toString()}))); @@ -804,9 +804,9 @@ class InputModel { } /// Send mouse press event. - void sendMouse(String type, MouseButtons button) { + Future sendMouse(String type, MouseButtons button) async { if (!keyboardPerm) return; - bind.sessionSendMouse( + await bind.sessionSendMouse( sessionId: sessionId, msg: json.encode(modify({'type': type, 'buttons': button.value}))); } @@ -830,11 +830,11 @@ class InputModel { } /// Send mouse movement event with distance in [x] and [y]. - void moveMouse(double x, double y) { + Future moveMouse(double x, double y) async { if (!keyboardPerm) return; var x2 = x.toInt(); var y2 = y.toInt(); - bind.sessionSendMouse( + await bind.sessionSendMouse( sessionId: sessionId, msg: json.encode(modify({'x': '$x2', 'y': '$y2'}))); } diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index f68801f583e4..d3c76457a8a9 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -2038,7 +2038,7 @@ class CursorModel with ChangeNotifier { } // For touch mode - move(double x, double y) { + Future move(double x, double y) async { if (shouldBlock(x, y)) { _lastIsBlocked = true; return false; @@ -2047,7 +2047,7 @@ class CursorModel with ChangeNotifier { if (!_moveLocalIfInRemoteRect(x, y)) { return false; } - parent.target?.inputModel.moveMouse(_x, _y); + await parent.target?.inputModel.moveMouse(_x, _y); return true; } @@ -2105,9 +2105,9 @@ class CursorModel with ChangeNotifier { notifyListeners(); } - updatePan(Offset delta, Offset localPosition, bool touchMode) { + updatePan(Offset delta, Offset localPosition, bool touchMode) async { if (touchMode) { - _handleTouchMode(delta, localPosition); + await _handleTouchMode(delta, localPosition); return; } double dx = delta.dx; @@ -2205,7 +2205,7 @@ class CursorModel with ChangeNotifier { return x >= 0 && y >= 0 && x <= w && y <= h; } - _handleTouchMode(Offset delta, Offset localPosition) { + _handleTouchMode(Offset delta, Offset localPosition) async { bool isMoved = false; if (_remoteWindowCoords.isNotEmpty && _windowRect != null && @@ -2221,7 +2221,7 @@ class CursorModel with ChangeNotifier { coords.canvas.scale; x2 += coords.cursor.offset.dx; y2 += coords.cursor.offset.dy; - parent.target?.inputModel.moveMouse(x2, y2); + await parent.target?.inputModel.moveMouse(x2, y2); isMoved = true; } } @@ -2264,7 +2264,7 @@ class CursorModel with ChangeNotifier { _x = movement.dx; _y = movement.dy; - parent.target?.inputModel.moveMouse(_x, _y); + await parent.target?.inputModel.moveMouse(_x, _y); } notifyListeners(); } From 6f0cb3b8c2f9381f6fadf3048117ef1843b0e8b2 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Thu, 7 Nov 2024 22:36:56 +0800 Subject: [PATCH 357/541] fix: mobile, two fingers cale, no tapdown (#9856) Signed-off-by: fufesou --- flutter/lib/common/widgets/remote_input.dart | 27 +++++++++++++------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/flutter/lib/common/widgets/remote_input.dart b/flutter/lib/common/widgets/remote_input.dart index fa635222d47b..c31350b047c0 100644 --- a/flutter/lib/common/widgets/remote_input.dart +++ b/flutter/lib/common/widgets/remote_input.dart @@ -84,6 +84,9 @@ class _RawTouchGestureDetectorRegionState double _mouseScrollIntegral = 0; // mouse scroll speed controller double _scale = 1; + // Workaround tap down event when two fingers are used to scale(mobile) + TapDownDetails? _lastTapDownDetails; + PointerDeviceKind? lastDeviceKind; // For touch mode, onDoubleTap @@ -114,15 +117,13 @@ class _RawTouchGestureDetectorRegionState if (handleTouch) { _lastPosOfDoubleTapDown = d.localPosition; // Desktop or mobile "Touch mode" - final isMoved = - await ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy); - if (isMoved) { - await inputModel.tapDown(MouseButtons.left); - } + _lastTapDownDetails = d; } } onTapUp(TapUpDetails d) async { + final TapDownDetails? lastTapDownDetails = _lastTapDownDetails; + _lastTapDownDetails = null; if (lastDeviceKind != PointerDeviceKind.touch) { return; } @@ -130,7 +131,10 @@ class _RawTouchGestureDetectorRegionState final isMoved = await ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy); if (isMoved) { - inputModel.tapUp(MouseButtons.left); + if (lastTapDownDetails != null) { + await inputModel.tapDown(MouseButtons.left); + } + await inputModel.tapUp(MouseButtons.left); } } } @@ -179,9 +183,7 @@ class _RawTouchGestureDetectorRegionState if (handleTouch) { _lastPosOfDoubleTapDown = d.localPosition; _cacheLongPressPosition = d.localPosition; - final isMoved = - await ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy); - if (!isMoved) { + if (!ffi.cursorModel.isInRemoteRect(d.localPosition)) { return; } _cacheLongPressPositionTs = DateTime.now().millisecondsSinceEpoch; @@ -268,11 +270,17 @@ class _RawTouchGestureDetectorRegionState } onOneFingerPanStart(BuildContext context, DragStartDetails d) async { + final TapDownDetails? lastTapDownDetails = _lastTapDownDetails; + _lastTapDownDetails = null; lastDeviceKind = d.kind ?? lastDeviceKind; if (lastDeviceKind != PointerDeviceKind.touch) { return; } if (handleTouch) { + if (lastTapDownDetails != null) { + await ffi.cursorModel.move(lastTapDownDetails.localPosition.dx, + lastTapDownDetails.localPosition.dy); + } if (ffi.cursorModel.shouldBlock(d.localPosition.dx, d.localPosition.dy)) { return; } @@ -336,6 +344,7 @@ class _RawTouchGestureDetectorRegionState // scale + pan event onTwoFingerScaleStart(ScaleStartDetails d) { + _lastTapDownDetails = null; if (lastDeviceKind != PointerDeviceKind.touch) { return; } From 0f070b0108554ef9d7385c09120ff298770771ec Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Thu, 7 Nov 2024 22:54:14 +0800 Subject: [PATCH 358/541] revert: 9644, iOS, Korean input (#9857) Signed-off-by: fufesou --- flutter/lib/mobile/pages/remote_page.dart | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index a1be9c009b95..fd0e007e288a 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -269,14 +269,10 @@ class _RemotePageState extends State with WidgetsBindingObserver { } } - Future handleSoftKeyboardInput(String newValue) async { + // handle mobile virtual keyboard + void handleSoftKeyboardInput(String newValue) { if (isIOS) { - // fix: TextFormField onChanged event triggered multiple times when Korean input - // https://github.com/rustdesk/rustdesk/pull/9644 - await Future.delayed(const Duration(milliseconds: 10)); - - if (newValue != _textController.text) return; - _handleIOSSoftKeyboardInput(_textController.text); + _handleIOSSoftKeyboardInput(newValue); } else { _handleNonIOSSoftKeyboardInput(newValue); } From 7978e0301d613e273eecffde4184d4c14e2c196d Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Fri, 8 Nov 2024 12:11:56 +0800 Subject: [PATCH 359/541] fix: input mobile -> Android (#9767) Signed-off-by: fufesou --- .../com/carriez/flutter_hbb/InputService.kt | 24 ++++++++++++------- .../flutter_hbb/KeyboardKeyEventMapper.kt | 4 +--- flutter/lib/mobile/pages/remote_page.dart | 8 +++++++ 3 files changed, 25 insertions(+), 11 deletions(-) diff --git a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/InputService.kt b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/InputService.kt index 3fcc72df346d..f53f95d6754e 100644 --- a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/InputService.kt +++ b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/InputService.kt @@ -280,20 +280,20 @@ class InputService : AccessibilityService() { var textToCommit: String? = null - if (keyboardMode == KeyboardMode.Legacy) { - if (keyEvent.hasChr() && keyEvent.getDown()) { + // [down] indicates the key's state(down or up). + // [press] indicates a click event(down and up). + // https://github.com/rustdesk/rustdesk/blob/3a7594755341f023f56fa4b6a43b60d6b47df88d/flutter/lib/models/input_model.dart#L688 + if (keyEvent.hasSeq()) { + textToCommit = keyEvent.getSeq() + } else if (keyboardMode == KeyboardMode.Legacy) { + if (keyEvent.hasChr() && (keyEvent.getDown() || keyEvent.getPress())) { val chr = keyEvent.getChr() if (chr != null) { textToCommit = String(Character.toChars(chr)) } } } else if (keyboardMode == KeyboardMode.Translate) { - if (keyEvent.hasSeq() && keyEvent.getDown()) { - val seq = keyEvent.getSeq() - if (seq != null) { - textToCommit = seq - } - } + } else { } Log.d(logTag, "onKeyEvent $keyEvent textToCommit:$textToCommit") @@ -320,6 +320,10 @@ class InputService : AccessibilityService() { } else { ke?.let { event -> inputConnection.sendKeyEvent(event) + if (keyEvent.getPress()) { + val actionUpEvent = KeyEventAndroid(KeyEventAndroid.ACTION_UP, event.keyCode) + inputConnection.sendKeyEvent(actionUpEvent) + } } } } @@ -333,6 +337,10 @@ class InputService : AccessibilityService() { for (item in possibleNodes) { val success = trySendKeyEvent(event, item, textToCommit) if (success) { + if (keyEvent.getPress()) { + val actionUpEvent = KeyEventAndroid(KeyEventAndroid.ACTION_UP, event.keyCode) + trySendKeyEvent(actionUpEvent, item, textToCommit) + } break } } diff --git a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/KeyboardKeyEventMapper.kt b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/KeyboardKeyEventMapper.kt index 1e63df4054f5..ccb33195e9fb 100644 --- a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/KeyboardKeyEventMapper.kt +++ b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/KeyboardKeyEventMapper.kt @@ -31,14 +31,12 @@ object KeyEventConverter { } var action = 0 - if (keyEventProto.getDown()) { + if (keyEventProto.getDown() || keyEventProto.getPress()) { action = KeyEvent.ACTION_DOWN } else { 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) } diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index fd0e007e288a..e63a5c5b9bf4 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -560,6 +560,14 @@ class _RemotePageState extends State with WidgetsBindingObserver { controller: _textController, // trick way to make backspace work always keyboardType: TextInputType.multiline, + // `onChanged` may be called depending on the input method if this widget is wrapped in + // `Focus(onKeyEvent: ..., child: ...)` + // For `Backspace` button in the soft keyboard: + // en/fr input method: + // 1. The button will not trigger `onKeyEvent` if the text field is not empty. + // 2. The button will trigger `onKeyEvent` if the text field is empty. + // ko/zh/ja input method: the button will trigger `onKeyEvent` + // and the event will not popup if `KeyEventResult.handled` is returned. onChanged: handleSoftKeyboardInput, ), ), From 740c5358abb1a6a178ddd8b93a0c50f896b9bf34 Mon Sep 17 00:00:00 2001 From: 21pages Date: Fri, 8 Nov 2024 12:12:10 +0800 Subject: [PATCH 360/541] rotate ID3D11Texture2D (#9772) * Rotate ID3D11Texture2D after duplication with d3d11 video processor. * If display is not rotated, nothing will be created; If the rotation fails, it will use the old fallback logic TODO: * If changing from Landscape to Landscape(flipped) during capture, the resolution is not changed, video service fallback to gdi directly. Signed-off-by: 21pages --- libs/scrap/src/dxgi/mod.rs | 242 +++++++++++++++++++++++++++++++++--- src/server/connection.rs | 8 +- src/server/video_service.rs | 1 + 3 files changed, 236 insertions(+), 15 deletions(-) diff --git a/libs/scrap/src/dxgi/mod.rs b/libs/scrap/src/dxgi/mod.rs index 33a60e7d9919..9220fa605939 100644 --- a/libs/scrap/src/dxgi/mod.rs +++ b/libs/scrap/src/dxgi/mod.rs @@ -7,10 +7,11 @@ use winapi::{ shared::{ dxgi::*, dxgi1_2::*, + dxgiformat::DXGI_FORMAT_B8G8R8A8_UNORM, dxgitype::*, minwindef::{DWORD, FALSE, TRUE, UINT}, ntdef::LONG, - windef::HMONITOR, + windef::{HMONITOR, RECT}, winerror::*, // dxgiformat::{DXGI_FORMAT, DXGI_FORMAT_B8G8R8A8_UNORM, DXGI_FORMAT_420_OPAQUE}, }, @@ -57,6 +58,7 @@ pub struct Capturer { saved_raw_data: Vec, // for faster compare and copy output_texture: bool, adapter_desc1: DXGI_ADAPTER_DESC1, + rotate: Rotate, } impl Capturer { @@ -151,6 +153,7 @@ impl Capturer { (*duplication).GetDesc(&mut desc); } } + let rotate = Self::create_rotations(device.0, context.0, &display); Ok(Capturer { device, @@ -168,9 +171,143 @@ impl Capturer { saved_raw_data: Vec::new(), output_texture: false, adapter_desc1, + rotate, }) } + fn create_rotations( + device: *mut ID3D11Device, + context: *mut ID3D11DeviceContext, + display: &Display, + ) -> Rotate { + let mut video_context: *mut ID3D11VideoContext = ptr::null_mut(); + let mut video_device: *mut ID3D11VideoDevice = ptr::null_mut(); + let mut video_processor_enum: *mut ID3D11VideoProcessorEnumerator = ptr::null_mut(); + let mut video_processor: *mut ID3D11VideoProcessor = ptr::null_mut(); + let processor_rotation = match display.rotation() { + DXGI_MODE_ROTATION_ROTATE90 => Some(D3D11_VIDEO_PROCESSOR_ROTATION_90), + DXGI_MODE_ROTATION_ROTATE180 => Some(D3D11_VIDEO_PROCESSOR_ROTATION_180), + DXGI_MODE_ROTATION_ROTATE270 => Some(D3D11_VIDEO_PROCESSOR_ROTATION_270), + _ => None, + }; + if let Some(processor_rotation) = processor_rotation { + println!("create rotations"); + if !device.is_null() && !context.is_null() { + unsafe { + (*context).QueryInterface( + &IID_ID3D11VideoContext, + &mut video_context as *mut *mut _ as *mut *mut _, + ); + if !video_context.is_null() { + (*device).QueryInterface( + &IID_ID3D11VideoDevice, + &mut video_device as *mut *mut _ as *mut *mut _, + ); + if !video_device.is_null() { + let (input_width, input_height) = match display.rotation() { + DXGI_MODE_ROTATION_ROTATE90 | DXGI_MODE_ROTATION_ROTATE270 => { + (display.height(), display.width()) + } + _ => (display.width(), display.height()), + }; + let (output_width, output_height) = (display.width(), display.height()); + let content_desc = D3D11_VIDEO_PROCESSOR_CONTENT_DESC { + InputFrameFormat: D3D11_VIDEO_FRAME_FORMAT_PROGRESSIVE, + InputFrameRate: DXGI_RATIONAL { + Numerator: 30, + Denominator: 1, + }, + InputWidth: input_width as _, + InputHeight: input_height as _, + OutputFrameRate: DXGI_RATIONAL { + Numerator: 30, + Denominator: 1, + }, + OutputWidth: output_width as _, + OutputHeight: output_height as _, + Usage: D3D11_VIDEO_USAGE_PLAYBACK_NORMAL, + }; + (*video_device).CreateVideoProcessorEnumerator( + &content_desc, + &mut video_processor_enum, + ); + if !video_processor_enum.is_null() { + let mut caps: D3D11_VIDEO_PROCESSOR_CAPS = mem::zeroed(); + if S_OK == (*video_processor_enum).GetVideoProcessorCaps(&mut caps) + { + if caps.FeatureCaps + & D3D11_VIDEO_PROCESSOR_FEATURE_CAPS_ROTATION + != 0 + { + (*video_device).CreateVideoProcessor( + video_processor_enum, + 0, + &mut video_processor, + ); + if !video_processor.is_null() { + (*video_context).VideoProcessorSetStreamRotation( + video_processor, + 0, + TRUE, + processor_rotation, + ); + (*video_context) + .VideoProcessorSetStreamAutoProcessingMode( + video_processor, + 0, + FALSE, + ); + (*video_context).VideoProcessorSetStreamFrameFormat( + video_processor, + 0, + D3D11_VIDEO_FRAME_FORMAT_PROGRESSIVE, + ); + (*video_context).VideoProcessorSetStreamSourceRect( + video_processor, + 0, + TRUE, + &RECT { + left: 0, + top: 0, + right: input_width as _, + bottom: input_height as _, + }, + ); + (*video_context).VideoProcessorSetStreamDestRect( + video_processor, + 0, + TRUE, + &RECT { + left: 0, + top: 0, + right: output_width as _, + bottom: output_height as _, + }, + ); + } + } + } + } + } + } + } + } + } + + let video_context = ComPtr(video_context); + let video_device = ComPtr(video_device); + let video_processor_enum = ComPtr(video_processor_enum); + let video_processor = ComPtr(video_processor); + let rotated_texture = ComPtr(ptr::null_mut()); + Rotate { + video_context, + video_device, + video_processor_enum, + video_processor, + texture: (rotated_texture, false), + } + } + pub fn is_gdi(&self) -> bool { self.gdi_capturer.is_some() } @@ -253,17 +390,7 @@ impl Capturer { pub fn frame<'a>(&'a mut self, timeout: UINT) -> io::Result> { if self.output_texture { - let rotation = match self.display.rotation() { - DXGI_MODE_ROTATION_IDENTITY | DXGI_MODE_ROTATION_UNSPECIFIED => 0, - DXGI_MODE_ROTATION_ROTATE90 => 90, - DXGI_MODE_ROTATION_ROTATE180 => 180, - DXGI_MODE_ROTATION_ROTATE270 => 270, - _ => { - // Unsupported rotation, try anyway - 0 - } - }; - Ok(Frame::Texture((self.get_texture(timeout)?, rotation))) + Ok(Frame::Texture(self.get_texture(timeout)?)) } else { let width = self.width; let height = self.height; @@ -338,7 +465,7 @@ impl Capturer { } } - fn get_texture(&mut self, timeout: UINT) -> io::Result<*mut c_void> { + fn get_texture(&mut self, timeout: UINT) -> io::Result<(*mut c_void, usize)> { unsafe { if self.duplication.0.is_null() { return Err(std::io::ErrorKind::AddrNotAvailable.into()); @@ -362,7 +489,86 @@ impl Capturer { ); let texture = ComPtr(texture); self.texture = texture; - Ok(self.texture.0 as *mut c_void) + + let mut final_texture = self.texture.0 as *mut c_void; + let mut rotation = match self.display.rotation() { + DXGI_MODE_ROTATION_ROTATE90 => 90, + DXGI_MODE_ROTATION_ROTATE180 => 180, + DXGI_MODE_ROTATION_ROTATE270 => 270, + _ => 0, + }; + if rotation != 0 + && !self.texture.is_null() + && !self.rotate.video_context.is_null() + && !self.rotate.video_device.is_null() + && !self.rotate.video_processor_enum.is_null() + && !self.rotate.video_processor.is_null() + { + let mut desc: D3D11_TEXTURE2D_DESC = mem::zeroed(); + (*self.texture.0).GetDesc(&mut desc); + if rotation == 90 || rotation == 270 { + let tmp = desc.Width; + desc.Width = desc.Height; + desc.Height = tmp; + } + if !self.rotate.texture.1 { + self.rotate.texture.1 = true; + let mut rotated_texture: *mut ID3D11Texture2D = ptr::null_mut(); + desc.MiscFlags = D3D11_RESOURCE_MISC_SHARED; + (*self.device.0).CreateTexture2D(&desc, ptr::null(), &mut rotated_texture); + self.rotate.texture.0 = ComPtr(rotated_texture); + } + if !self.rotate.texture.0.is_null() + && desc.Width == self.width as u32 + && desc.Height == self.height as u32 + { + let input_view_desc = D3D11_VIDEO_PROCESSOR_INPUT_VIEW_DESC { + FourCC: 0, + ViewDimension: D3D11_VPIV_DIMENSION_TEXTURE2D, + Texture2D: D3D11_TEX2D_VPIV { + ArraySlice: 0, + MipSlice: 0, + }, + }; + let mut input_view = ptr::null_mut(); + (*self.rotate.video_device.0).CreateVideoProcessorInputView( + self.texture.0 as *mut _, + self.rotate.video_processor_enum.0 as *mut _, + &input_view_desc, + &mut input_view, + ); + if !input_view.is_null() { + let input_view = ComPtr(input_view); + let mut output_view_desc: D3D11_VIDEO_PROCESSOR_OUTPUT_VIEW_DESC = + mem::zeroed(); + output_view_desc.ViewDimension = D3D11_VPOV_DIMENSION_TEXTURE2D; + output_view_desc.u.Texture2D_mut().MipSlice = 0; + let mut output_view = ptr::null_mut(); + (*self.rotate.video_device.0).CreateVideoProcessorOutputView( + self.rotate.texture.0 .0 as *mut _, + self.rotate.video_processor_enum.0 as *mut _, + &output_view_desc, + &mut output_view, + ); + if !output_view.is_null() { + let output_view = ComPtr(output_view); + let mut stream_data: D3D11_VIDEO_PROCESSOR_STREAM = mem::zeroed(); + stream_data.Enable = TRUE; + stream_data.pInputSurface = input_view.0; + (*self.rotate.video_context.0).VideoProcessorBlt( + self.rotate.video_processor.0, + output_view.0, + 0, + 1, + &stream_data, + ); + final_texture = self.rotate.texture.0 .0 as *mut c_void; + rotation = 0; + } + } + } + } + Ok((final_texture, rotation)) } } @@ -666,3 +872,11 @@ fn wrap_hresult(x: HRESULT) -> io::Result<()> { }) .into()) } + +struct Rotate { + video_context: ComPtr, + video_device: ComPtr, + video_processor_enum: ComPtr, + video_processor: ComPtr, + texture: (ComPtr, bool), +} diff --git a/src/server/connection.rs b/src/server/connection.rs index c0cf8c784e68..3315cac6972d 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -3838,6 +3838,12 @@ mod raii { let mut lock = SESSIONS.lock().unwrap(); let contains = lock.contains_key(&key); if contains { + // No two remote connections with the same session key, just for ensure. + let is_remote = AUTHED_CONNS + .lock() + .unwrap() + .iter() + .any(|c| c.0 == conn_id && c.1 == AuthConnType::Remote); // If there are 2 connections with the same peer_id and session_id, a remote connection and a file transfer or port forward connection, // If any of the connections is closed allowing retry, this will not be called; // If the file transfer/port forward connection is closed with no retry, the session should be kept for remote control menu action; @@ -3847,7 +3853,7 @@ mod raii { .unwrap() .iter() .any(|c| c.0 != conn_id && c.2 == key && c.1 == AuthConnType::Remote); - if !another_remote { + if is_remote || !another_remote { lock.remove(&key); log::info!("remove session"); } else { diff --git a/src/server/video_service.rs b/src/server/video_service.rs index 55bfa08f0e69..e6eed86cc5b0 100644 --- a/src/server/video_service.rs +++ b/src/server/video_service.rs @@ -971,6 +971,7 @@ fn handle_one_frame( } match e.to_string().as_str() { scrap::codec::ENCODE_NEED_SWITCH => { + encoder.disable(); log::error!("switch due to encoder need switch"); bail!("SWITCH"); } From a277b022ffc166de1db0c4882a7b7642a4453c86 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Fri, 8 Nov 2024 15:00:49 +0800 Subject: [PATCH 361/541] bump to 1.3.3 --- .github/workflows/flutter-build.yml | 2 +- .github/workflows/playground.yml | 2 +- Cargo.lock | 4 ++-- Cargo.toml | 2 +- appimage/AppImageBuilder-aarch64.yml | 2 +- appimage/AppImageBuilder-x86_64.yml | 2 +- flutter/pubspec.lock | 4 ++-- flutter/pubspec.yaml | 4 ++-- libs/portable/Cargo.toml | 2 +- res/PKGBUILD | 2 +- res/rpm-flutter-suse.spec | 2 +- res/rpm-flutter.spec | 2 +- res/rpm.spec | 2 +- 13 files changed, 16 insertions(+), 16 deletions(-) diff --git a/.github/workflows/flutter-build.yml b/.github/workflows/flutter-build.yml index dd21515ba547..5c2c608a821d 100644 --- a/.github/workflows/flutter-build.yml +++ b/.github/workflows/flutter-build.yml @@ -33,7 +33,7 @@ env: VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite" # vcpkg version: 2024.07.12 VCPKG_COMMIT_ID: "1de2026f28ead93ff1773e6e680387643e914ea1" - VERSION: "1.3.2" + VERSION: "1.3.3" NDK_VERSION: "r27b" #signing keys env variable checks ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}" diff --git a/.github/workflows/playground.yml b/.github/workflows/playground.yml index efd6974a9914..843e07835cdb 100644 --- a/.github/workflows/playground.yml +++ b/.github/workflows/playground.yml @@ -18,7 +18,7 @@ env: VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite" # vcpkg version: 2024.06.15 VCPKG_COMMIT_ID: "f7423ee180c4b7f40d43402c2feb3859161ef625" - VERSION: "1.3.2" + VERSION: "1.3.3" NDK_VERSION: "r26d" #signing keys env variable checks ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}" diff --git a/Cargo.lock b/Cargo.lock index 7565d34eed64..5930b87216d4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5480,7 +5480,7 @@ dependencies = [ [[package]] name = "rustdesk" -version = "1.3.2" +version = "1.3.3" dependencies = [ "android-wakelock", "android_logger", @@ -5580,7 +5580,7 @@ dependencies = [ [[package]] name = "rustdesk-portable-packer" -version = "1.3.2" +version = "1.3.3" dependencies = [ "brotli", "dirs 5.0.1", diff --git a/Cargo.toml b/Cargo.toml index 1ee749afb80e..a7788372b958 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rustdesk" -version = "1.3.2" +version = "1.3.3" authors = ["rustdesk "] edition = "2021" build= "build.rs" diff --git a/appimage/AppImageBuilder-aarch64.yml b/appimage/AppImageBuilder-aarch64.yml index 5ff9fc2a7b23..a6bd632dc0cb 100644 --- a/appimage/AppImageBuilder-aarch64.yml +++ b/appimage/AppImageBuilder-aarch64.yml @@ -18,7 +18,7 @@ AppDir: id: rustdesk name: rustdesk icon: rustdesk - version: 1.3.2 + version: 1.3.3 exec: usr/lib/rustdesk/rustdesk exec_args: $@ apt: diff --git a/appimage/AppImageBuilder-x86_64.yml b/appimage/AppImageBuilder-x86_64.yml index d8f0991cf953..9ea820fec475 100644 --- a/appimage/AppImageBuilder-x86_64.yml +++ b/appimage/AppImageBuilder-x86_64.yml @@ -18,7 +18,7 @@ AppDir: id: rustdesk name: rustdesk icon: rustdesk - version: 1.3.2 + version: 1.3.3 exec: usr/lib/rustdesk/rustdesk exec_args: $@ apt: diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index 6551bbb37bfb..1a8badd89ae7 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -1583,8 +1583,8 @@ packages: dependency: "direct main" description: path: "plugins/window_size" - ref: a738913c8ce2c9f47515382d40827e794a334274 - resolved-ref: a738913c8ce2c9f47515382d40827e794a334274 + ref: eb3964990cf19629c89ff8cb4a37640c7b3d5601 + resolved-ref: eb3964990cf19629c89ff8cb4a37640c7b3d5601 url: "https://github.com/google/flutter-desktop-embedding.git" source: git version: "0.1.0" diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index cc3a2e6c53f7..0f105d702bf3 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # 1.1.9-1 works for android, but for ios it becomes 1.1.91, need to set it to 1.1.9-a.1 for iOS, will get 1.1.9.1, but iOS store not allow 4 numbers -version: 1.3.2+51 +version: 1.3.3+52 environment: sdk: '^3.1.0' @@ -62,7 +62,7 @@ dependencies: git: url: https://github.com/google/flutter-desktop-embedding.git path: plugins/window_size - ref: a738913c8ce2c9f47515382d40827e794a334274 + ref: eb3964990cf19629c89ff8cb4a37640c7b3d5601 get: ^4.6.5 visibility_detector: ^0.4.0+2 contextmenu: ^3.0.0 diff --git a/libs/portable/Cargo.toml b/libs/portable/Cargo.toml index 7e60f7d1fac6..3bf827865dd6 100644 --- a/libs/portable/Cargo.toml +++ b/libs/portable/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rustdesk-portable-packer" -version = "1.3.2" +version = "1.3.3" edition = "2021" description = "RustDesk Remote Desktop" diff --git a/res/PKGBUILD b/res/PKGBUILD index 616682e8f69f..3da7c98da0ab 100644 --- a/res/PKGBUILD +++ b/res/PKGBUILD @@ -1,5 +1,5 @@ pkgname=rustdesk -pkgver=1.3.2 +pkgver=1.3.3 pkgrel=0 epoch= pkgdesc="" diff --git a/res/rpm-flutter-suse.spec b/res/rpm-flutter-suse.spec index 768b04c28d2e..7f52a8f57f87 100644 --- a/res/rpm-flutter-suse.spec +++ b/res/rpm-flutter-suse.spec @@ -1,5 +1,5 @@ Name: rustdesk -Version: 1.3.2 +Version: 1.3.3 Release: 0 Summary: RPM package License: GPL-3.0 diff --git a/res/rpm-flutter.spec b/res/rpm-flutter.spec index b62c18b3b71f..fcb15a1a14ab 100644 --- a/res/rpm-flutter.spec +++ b/res/rpm-flutter.spec @@ -1,5 +1,5 @@ Name: rustdesk -Version: 1.3.2 +Version: 1.3.3 Release: 0 Summary: RPM package License: GPL-3.0 diff --git a/res/rpm.spec b/res/rpm.spec index 033e95937d25..bdd6afa3f4f6 100644 --- a/res/rpm.spec +++ b/res/rpm.spec @@ -1,5 +1,5 @@ Name: rustdesk -Version: 1.3.2 +Version: 1.3.3 Release: 0 Summary: RPM package License: GPL-3.0 From d3efcd4223a0687181616ef53d64bbea6c164f1a Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Fri, 8 Nov 2024 15:01:36 +0800 Subject: [PATCH 362/541] fix: mobile, soft keyboard (#9860) Switching the input method, don't affect the canvas. Signed-off-by: fufesou --- flutter/lib/mobile/pages/remote_page.dart | 9 ++++++++- flutter/lib/models/model.dart | 16 ++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index e63a5c5b9bf4..1dee69b94ee7 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -134,6 +134,13 @@ class _RemotePageState extends State with WidgetsBindingObserver { @override void didChangeMetrics() { + // If the soft keyboard is visible and the canvas has been changed(panned or scaled) + // Don't try reset the view style and focus the cursor. + if (gFFI.cursorModel.lastKeyboardIsVisible && + gFFI.canvasModel.isMobileCanvasChanged) { + return; + } + final newBottom = MediaQueryData.fromView(ui.window).viewInsets.bottom; _timerDidChangeMetrics?.cancel(); _timerDidChangeMetrics = Timer(Duration(milliseconds: 100), () async { @@ -563,7 +570,7 @@ class _RemotePageState extends State with WidgetsBindingObserver { // `onChanged` may be called depending on the input method if this widget is wrapped in // `Focus(onKeyEvent: ..., child: ...)` // For `Backspace` button in the soft keyboard: - // en/fr input method: + // en/fr input method: // 1. The button will not trigger `onKeyEvent` if the text field is not empty. // 2. The button will trigger `onKeyEvent` if the text field is empty. // ko/zh/ja input method: the button will trigger `onKeyEvent` diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index d3c76457a8a9..5446856fe5ab 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -1424,6 +1424,10 @@ class CanvasModel with ChangeNotifier { Timer? _timerMobileFocusCanvasCursor; + // `isMobileCanvasChanged` is used to avoid canvas reset when changing the input method + // after showing the soft keyboard. + bool isMobileCanvasChanged = false; + final ScrollController _horizontal = ScrollController(); final ScrollController _vertical = ScrollController(); @@ -1639,6 +1643,9 @@ class CanvasModel with ChangeNotifier { panX(double dx) { _x += dx; + if (isMobile) { + isMobileCanvasChanged = true; + } notifyListeners(); } @@ -1653,6 +1660,9 @@ class CanvasModel with ChangeNotifier { panY(double dy) { _y += dy; + if (isMobile) { + isMobileCanvasChanged = true; + } notifyListeners(); } @@ -1672,6 +1682,9 @@ class CanvasModel with ChangeNotifier { // (focalPoint.dy - _y_1 - adjust) / s1 + displayOriginY = (focalPoint.dy - _y_2 - adjust) / s2 + displayOriginY // _y_2 = focalPoint.dy - adjust - (focalPoint.dy - _y_1 - adjust) / s1 * s2 _y = focalPoint.dy - adjust - (focalPoint.dy - _y - adjust) / s * _scale; + if (isMobile) { + isMobileCanvasChanged = true; + } notifyListeners(); } @@ -1941,6 +1954,8 @@ class CursorModel with ChangeNotifier { bool _lastIsBlocked = false; bool _lastKeyboardIsVisible = false; + bool get lastKeyboardIsVisible => _lastKeyboardIsVisible; + Rect? get keyHelpToolsRectToAdjustCanvas => _lastKeyboardIsVisible ? _keyHelpToolsRect : null; keyHelpToolsVisibilityChanged(Rect? r, bool keyboardIsVisible) { @@ -1955,6 +1970,7 @@ class CursorModel with ChangeNotifier { } if (isMobile && _lastKeyboardIsVisible != keyboardIsVisible) { parent.target?.canvasModel.mobileFocusCanvasCursor(); + parent.target?.canvasModel.isMobileCanvasChanged = false; } _lastKeyboardIsVisible = keyboardIsVisible; } From 68b07505ab4bae7d10fcc63b5a9c767d83385888 Mon Sep 17 00:00:00 2001 From: XLion Date: Fri, 8 Nov 2024 22:18:30 +0800 Subject: [PATCH 363/541] Update tw.rs (#9863) --- src/lang/tw.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 8c644d6cfea2..86aad4fee2b8 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -652,6 +652,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Download", "下載"), ("Upload folder", "上傳資料夾"), ("Upload files", "上傳檔案"), - ("Clipboard is synchronized", ""), + ("Clipboard is synchronized", "剪貼簿已同步"), ].iter().cloned().collect(); } From 062c8d582cf4e805481a08df0912fb239d495167 Mon Sep 17 00:00:00 2001 From: Lee Jong Mun <43285072+crwusiz@users.noreply.github.com> Date: Sat, 9 Nov 2024 12:37:54 +0900 Subject: [PATCH 364/541] kor translation update (#9866) --- src/lang/ko.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 9b8c08d71171..4960e752ab6b 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -364,7 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recording", "녹화"), ("Directory", "경로"), ("Automatically record incoming sessions", "들어오는 세션을 자동으로 녹화"), - ("Automatically record outgoing sessions", ""), + ("Automatically record outgoing sessions", "나가는 세션을 자동으로 녹화"), ("Change", "변경"), ("Start session recording", "세션 녹화 시작"), ("Stop session recording", "세션 녹화 중지"), From 5eb2c31207fdbc8d6bf868b187aef15d9dbf28d7 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Sat, 9 Nov 2024 20:51:47 +0800 Subject: [PATCH 365/541] Refact/flutter 3.24.4 (#9870) * Update pubspec.lock Signed-off-by: fufesou * refact: flutter 3.24.3 Signed-off-by: fufesou * fix: workaround Autocomplete options Signed-off-by: fufesou * Replace engine with rustdesk custom flutter engine * Update flutter-build.yml to use RustDesk flutter engine * Fix the problem of missing extraction file directory windows-x64-release * Update pubspec.lock.3.22.3 Signed-off-by: fufesou * remove pubspec.lock.3.22.3 Signed-off-by: fufesou * upgrade flutter android to 3.24.4 Signed-off-by: fufesou --------- Signed-off-by: fufesou Co-authored-by: vitoway Co-authored-by: vitoway <167743630+vitoway@users.noreply.github.com> --- .github/workflows/bridge.yml | 37 ++++++- .github/workflows/flutter-build.yml | 103 ++++++++++-------- flutter/android/app/build.gradle | 17 +-- flutter/android/build.gradle | 17 +-- .../gradle/wrapper/gradle-wrapper.properties | 2 +- flutter/android/settings.gradle | 30 +++-- .../lib/desktop/pages/connection_page.dart | 11 +- flutter/pubspec.lock | 49 ++++----- flutter/pubspec.yaml | 9 +- 9 files changed, 153 insertions(+), 122 deletions(-) diff --git a/.github/workflows/bridge.yml b/.github/workflows/bridge.yml index 54180ccdd345..f06956c18330 100644 --- a/.github/workflows/bridge.yml +++ b/.github/workflows/bridge.yml @@ -6,7 +6,7 @@ on: workflow_call: env: - FLUTTER_VERSION: "3.19.6" + FLUTTER_VERSION: "3.22.3" FLUTTER_RUST_BRIDGE_VERSION: "1.80.1" RUST_VERSION: "1.75" # https://github.com/rustdesk/rustdesk/discussions/7503 @@ -22,11 +22,18 @@ jobs: os: ubuntu-20.04, extra-build-args: "", } + - { + target: aarch64-apple-darwin, + os: macos-latest, + arch: aarch64, + extra-build-args: "", + } steps: - name: Checkout source code uses: actions/checkout@v4 - name: Install prerequisites + if: matrix.job.os == 'ubuntu-20.04' run: | sudo apt-get install ca-certificates -y sudo apt-get update -y @@ -74,13 +81,22 @@ jobs: shell: bash run: | cargo install flutter_rust_bridge_codegen --version ${{ env.FLUTTER_RUST_BRIDGE_VERSION }} --features "uuid" - pushd flutter && flutter pub get && popd + pushd flutter && sed -i -e 's/extended_text: 14.0.0/extended_text: 13.0.0/g' pubspec.yaml && flutter pub get && popd - name: Run flutter rust bridge run: | - ~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart + case ${{ matrix.job.os }} in + ubuntu-20.04) + ~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart + ;; + macos-latest) + ~/.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 + ~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart --c-output ./flutter/ios/Runner/bridge_generated.h + ;; + esac - - name: Upload Artifact + - name: Upload Artifact(ubuntu) + if: matrix.job.os == 'ubuntu-20.04' uses: actions/upload-artifact@master with: name: bridge-artifact @@ -89,3 +105,16 @@ jobs: ./src/bridge_generated.io.rs ./flutter/lib/generated_bridge.dart ./flutter/lib/generated_bridge.freezed.dart + + - name: Upload Artifact(macos) + if: matrix.job.os == 'macos-latest' + uses: actions/upload-artifact@master + with: + name: bridge-artifact-macos + path: | + ./src/bridge_generated.rs + ./src/bridge_generated.io.rs + ./flutter/lib/generated_bridge.dart + ./flutter/lib/generated_bridge.freezed.dart + ./flutter/macos/Runner/bridge_generated.h + ./flutter/ios/Runner/bridge_generated.h diff --git a/.github/workflows/flutter-build.yml b/.github/workflows/flutter-build.yml index 5c2c608a821d..adfc14c86780 100644 --- a/.github/workflows/flutter-build.yml +++ b/.github/workflows/flutter-build.yml @@ -24,9 +24,8 @@ env: SCITER_ARMV7_CMAKE_VERSION: "3.29.7" SCITER_NASM_DEBVERSION: "2.14-1" LLVM_VERSION: "15.0.6" - FLUTTER_VERSION: "3.19.6" - ANDROID_FLUTTER_VERSION: "3.13.9" # >= 3.16 is very slow on my android phone, but work well on most of others. We may switch to new flutter after changing to texture rendering (I believe it can solve my problem). - FLUTTER_RUST_BRIDGE_VERSION: "1.80.1" + FLUTTER_VERSION: "3.24.4" + ANDROID_FLUTTER_VERSION: "3.24.4" # for arm64 linux because official Dart SDK does not work FLUTTER_ELINUX_VERSION: "3.16.9" TAG_NAME: "${{ inputs.upload-tag }}" @@ -46,6 +45,9 @@ env: SIGN_BASE_URL: "${{ secrets.SIGN_BASE_URL }}" jobs: + generate-bridge: + uses: ./.github/workflows/bridge.yml + build-RustDeskTempTopMostWindow: uses: ./.github/workflows/third-party-RustDeskTempTopMostWindow.yml with: @@ -59,7 +61,7 @@ jobs: build-for-windows-flutter: name: ${{ matrix.job.target }} - needs: [build-RustDeskTempTopMostWindow] + needs: [build-RustDeskTempTopMostWindow, generate-bridge] runs-on: ${{ matrix.job.os }} strategy: fail-fast: false @@ -85,6 +87,12 @@ jobs: - name: Checkout source code uses: actions/checkout@v4 + - name: Restore bridge files + uses: actions/download-artifact@master + with: + name: bridge-artifact + path: ./ + - name: Install LLVM and Clang uses: KyleMayes/install-llvm-action@v1 with: @@ -97,6 +105,15 @@ jobs: flutter-version: ${{ env.FLUTTER_VERSION }} cache: true + # https://github.com/flutter/flutter/issues/155685 + - name: Replace engine with rustdesk custom flutter engine + run: | + flutter doctor -v + flutter precache --windows + Invoke-WebRequest -Uri https://github.com/rustdesk/engine/releases/download/main/windows-x64-release.zip -OutFile windows-x64-release.zip + Expand-Archive -Path windows-x64-release.zip -DestinationPath windows-x64-release + mv -Force windows-x64-release/*  C:/hostedtoolcache/windows/flutter/stable-${{ env.FLUTTER_VERSION }}-x64/bin/cache/artifacts/engine/windows-x64-release/ + - name: Install Rust toolchain uses: dtolnay/rust-toolchain@v1 with: @@ -108,13 +125,6 @@ jobs: with: prefix-key: ${{ matrix.job.os }} - - 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" - 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: @@ -374,6 +384,7 @@ jobs: # use build-for-macOS instead if: false runs-on: [self-hosted, macOS, ARM64] + needs: [generate-bridge] steps: - name: Export GitHub Actions cache environment variables uses: actions/github-script@v6 @@ -385,12 +396,11 @@ jobs: - name: Checkout source code uses: actions/checkout@v4 - - name: Install flutter rust bridge deps - shell: bash - run: | - cargo install flutter_rust_bridge_codegen --version ${{ env.FLUTTER_RUST_BRIDGE_VERSION }} --features "uuid" - 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: Restore bridge files + uses: actions/download-artifact@master + with: + name: bridge-artifact-macos + path: ./ - name: Build rustdesk run: | @@ -446,6 +456,7 @@ jobs: if: ${{ inputs.upload-artifact }} name: build rustdesk ios ipa runs-on: ${{ matrix.job.os }} + needs: [generate-bridge] strategy: fail-fast: false matrix: @@ -510,12 +521,11 @@ jobs: prefix-key: rustdesk-lib-cache-ios key: ${{ matrix.job.target }} - - name: Install flutter rust bridge deps - shell: bash - run: | - cargo install flutter_rust_bridge_codegen --version ${{ env.FLUTTER_RUST_BRIDGE_VERSION }} --features "uuid" - 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/ios/Runner/bridge_generated.h + - name: Restore bridge files + uses: actions/download-artifact@master + with: + name: bridge-artifact-macos + path: ./ - name: Build rustdesk lib run: | @@ -550,6 +560,7 @@ jobs: #if: ${{ inputs.upload-artifact }} if: false runs-on: [self-hosted, macOS, ARM64] + needs: [generate-bridge] strategy: fail-fast: false steps: @@ -565,12 +576,11 @@ jobs: # $VCPKG_ROOT/vcpkg install --triplet arm64-ios --x-install-root="$VCPKG_ROOT/installed" - - name: Install flutter rust bridge deps - shell: bash - run: | - cargo install flutter_rust_bridge_codegen --version ${{ env.FLUTTER_RUST_BRIDGE_VERSION }} --features "uuid" - 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/ios/Runner/bridge_generated.h + - name: Restore bridge files + uses: actions/download-artifact@master + with: + name: bridge-artifact-macos + path: ./ - name: Build rustdesk lib run: | @@ -605,6 +615,7 @@ jobs: build-for-macOS: name: ${{ matrix.job.target }} runs-on: ${{ matrix.job.os }} + needs: [generate-bridge] strategy: fail-fast: false matrix: @@ -695,12 +706,11 @@ jobs: with: prefix-key: ${{ matrix.job.os }} - - name: Install flutter rust bridge deps - shell: bash - run: | - cargo install flutter_rust_bridge_codegen --version ${{ env.FLUTTER_RUST_BRIDGE_VERSION }} --features "uuid" - 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: Restore bridge files + uses: actions/download-artifact@master + with: + name: bridge-artifact-macos + path: ./ - name: Setup vcpkg with Github Actions binary cache uses: lukka/run-vcpkg@v11 @@ -822,11 +832,8 @@ jobs: tag_name: ${{ env.TAG_NAME }} files: rustdesk-${{ env.VERSION }}-unsigned.tar.gz - generate-bridge-linux: - uses: ./.github/workflows/bridge.yml - build-rustdesk-android: - needs: [generate-bridge-linux] + needs: [generate-bridge] name: build rustdesk android apk ${{ matrix.job.target }} runs-on: ${{ matrix.job.os }} strategy: @@ -903,7 +910,7 @@ jobs: llvm-10-dev \ nasm \ ninja-build \ - openjdk-11-jdk-headless \ + openjdk-17-jdk-headless \ pkg-config \ tree \ wget @@ -974,7 +981,7 @@ jobs: key: ${{ matrix.job.target }} - name: fix android for flutter 3.13 - if: $${{ env.ANDROID_FLUTTER_VERSION == '3.13.9' }} + if: ${{ env.ANDROID_FLUTTER_VERSION == '3.13.9' }} run: | cd flutter sed -i 's/uni_links_desktop/#uni_links_desktop/g' pubspec.yaml @@ -1022,9 +1029,9 @@ jobs: - name: Build rustdesk shell: bash env: - JAVA_HOME: /usr/lib/jvm/java-11-openjdk-amd64 + JAVA_HOME: /usr/lib/jvm/java-17-openjdk-amd64 run: | - export PATH=/usr/lib/jvm/java-11-openjdk-amd64/bin:$PATH + export PATH=/usr/lib/jvm/java-17-openjdk-amd64/bin:$PATH # temporary use debug sign config sed -i "s/signingConfigs.release/signingConfigs.debug/g" ./flutter/android/app/build.gradle case ${{ matrix.job.target }} in @@ -1166,7 +1173,7 @@ jobs: llvm-10-dev \ nasm \ ninja-build \ - openjdk-11-jdk-headless \ + openjdk-17-jdk-headless \ pkg-config \ tree \ wget @@ -1211,7 +1218,7 @@ jobs: path: ./flutter/android/app/src/main/jniLibs/x86 - name: fix android for flutter 3.13 - if: $${{ env.ANDROID_FLUTTER_VERSION == '3.13.9' }} + if: ${{ env.ANDROID_FLUTTER_VERSION == '3.13.9' }} run: | cd flutter sed -i 's/uni_links_desktop/#uni_links_desktop/g' pubspec.yaml @@ -1223,9 +1230,9 @@ jobs: - name: Build rustdesk shell: bash env: - JAVA_HOME: /usr/lib/jvm/java-11-openjdk-amd64 + JAVA_HOME: /usr/lib/jvm/java-17-openjdk-amd64 run: | - export PATH=/usr/lib/jvm/java-11-openjdk-amd64/bin:$PATH + export PATH=/usr/lib/jvm/java-17-openjdk-amd64/bin:$PATH # temporary use debug sign config sed -i "s/signingConfigs.release/signingConfigs.debug/g" ./flutter/android/app/build.gradle mv ./flutter/android/app/src/main/jniLibs/arm64-v8a/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/arm64-v8a/librustdesk.so @@ -1285,7 +1292,7 @@ jobs: signed-apk/rustdesk-${{ env.VERSION }}-universal${{ env.suffix }}.apk build-rustdesk-linux: - needs: [generate-bridge-linux] + needs: [generate-bridge] name: build rustdesk linux ${{ matrix.job.target }} runs-on: ${{ matrix.job.on }} strategy: diff --git a/flutter/android/app/build.gradle b/flutter/android/app/build.gradle index 320eb3347c0e..c554251656a8 100644 --- a/flutter/android/app/build.gradle +++ b/flutter/android/app/build.gradle @@ -1,6 +1,9 @@ import com.google.protobuf.gradle.* plugins { id "com.google.protobuf" version "0.9.4" + id "com.android.application" + id "kotlin-android" + id "dev.flutter.flutter-gradle-plugin" } def keystoreProperties = new Properties() @@ -17,11 +20,6 @@ if (localPropertiesFile.exists()) { } } -def flutterRoot = localProperties.getProperty('flutter.sdk') -if (flutterRoot == null) { - throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") -} - def flutterVersionCode = localProperties.getProperty('flutter.versionCode') if (flutterVersionCode == null) { flutterVersionCode = '1' @@ -32,10 +30,6 @@ if (flutterVersionName == null) { flutterVersionName = '1.0' } -apply plugin: 'com.android.application' -apply plugin: 'kotlin-android' -apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" - dependencies { implementation 'com.google.protobuf:protobuf-javalite:3.20.1' } @@ -57,7 +51,7 @@ protobuf { } android { - compileSdkVersion 33 + compileSdkVersion 34 sourceSets { main.java.srcDirs += 'src/main/kotlin' @@ -105,7 +99,6 @@ flutter { dependencies { implementation "androidx.media:media:1.6.0" 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("1.9.10") } } implementation 'com.caverock:androidsvg-aar:1.4' } - diff --git a/flutter/android/build.gradle b/flutter/android/build.gradle index c6a77f36b171..401bea0096e2 100644 --- a/flutter/android/build.gradle +++ b/flutter/android/build.gradle @@ -1,18 +1,3 @@ -buildscript { - ext.kotlin_version = '1.9.10' - repositories { - google() - jcenter() - maven { url 'https://jitpack.io' } - } - - dependencies { - classpath 'com.android.tools.build:gradle:7.0.0' - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - classpath 'com.google.gms:google-services:4.3.14' - } -} - allprojects { repositories { google() @@ -24,6 +9,8 @@ allprojects { rootProject.buildDir = '../build' subprojects { project.buildDir = "${rootProject.buildDir}/${project.name}" +} +subprojects { project.evaluationDependsOn(':app') } diff --git a/flutter/android/gradle/wrapper/gradle-wrapper.properties b/flutter/android/gradle/wrapper/gradle-wrapper.properties index cc5527d781a7..cb576305fbe9 100644 --- a/flutter/android/gradle/wrapper/gradle-wrapper.properties +++ b/flutter/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.4-all.zip diff --git a/flutter/android/settings.gradle b/flutter/android/settings.gradle index 44e62bcf06ae..c5fb685a1614 100644 --- a/flutter/android/settings.gradle +++ b/flutter/android/settings.gradle @@ -1,11 +1,25 @@ -include ':app' +pluginManagement { + def flutterSdkPath = { + def properties = new Properties() + file("local.properties").withInputStream { properties.load(it) } + def flutterSdkPath = properties.getProperty("flutter.sdk") + assert flutterSdkPath != null, "flutter.sdk not set in local.properties" + return flutterSdkPath + }() -def localPropertiesFile = new File(rootProject.projectDir, "local.properties") -def properties = new Properties() + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") -assert localPropertiesFile.exists() -localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} -def flutterSdkPath = properties.getProperty("flutter.sdk") -assert flutterSdkPath != null, "flutter.sdk not set in local.properties" -apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" +plugins { + id "dev.flutter.flutter-plugin-loader" version "1.0.0" + id "com.android.application" version "7.3.0" apply false + id "org.jetbrains.kotlin.android" version "1.9.10" apply false +} + +include ":app" diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index 744c05f9c223..e2681bb377d1 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -203,6 +203,8 @@ class _ConnectionPageState extends State bool isPeersLoading = false; bool isPeersLoaded = false; + // https://github.com/flutter/flutter/issues/157244 + Iterable _autocompleteOpts = []; @override void initState() { @@ -330,7 +332,7 @@ class _ConnectionPageState extends State child: Autocomplete( optionsBuilder: (TextEditingValue textEditingValue) { if (textEditingValue.text == '') { - return const Iterable.empty(); + _autocompleteOpts = const Iterable.empty(); } else if (peers.isEmpty && !isPeersLoaded) { Peer emptyPeer = Peer( id: '', @@ -346,7 +348,7 @@ class _ConnectionPageState extends State rdpUsername: '', loginName: '', ); - return [emptyPeer]; + _autocompleteOpts = [emptyPeer]; } else { String textWithoutSpaces = textEditingValue.text.replaceAll(" ", ""); @@ -357,8 +359,7 @@ class _ConnectionPageState extends State ); } String textToFind = textEditingValue.text.toLowerCase(); - - return peers + _autocompleteOpts = peers .where((peer) => peer.id.toLowerCase().contains(textToFind) || peer.username @@ -370,6 +371,7 @@ class _ConnectionPageState extends State peer.alias.toLowerCase().contains(textToFind)) .toList(); } + return _autocompleteOpts; }, fieldViewBuilder: ( BuildContext context, @@ -430,6 +432,7 @@ class _ConnectionPageState extends State optionsViewBuilder: (BuildContext context, AutocompleteOnSelected onSelected, Iterable options) { + options = _autocompleteOpts; double maxHeight = options.length * 50; if (options.length == 1) { maxHeight = 52; diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index 1a8badd89ae7..2f408f4eb423 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -37,10 +37,10 @@ packages: dependency: transitive description: name: archive - sha256: "22600aa1e926be775fa5fe7e6894e7fb3df9efda8891c73f70fb3262399a432d" + sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d url: "https://pub.dev" source: hosted - version: "3.4.10" + version: "3.6.1" args: dependency: transitive description: @@ -384,10 +384,10 @@ packages: dependency: "direct main" description: name: extended_text - sha256: "7f382de3af12992e34bd72ddd36becf90c4720900af126cb9859f0189af71ffe" + sha256: "38c1cac571d6eaf406f4b80040c1f88561e7617ad90795aac6a1be0a8d0bb676" url: "https://pub.dev" source: hosted - version: "13.0.0" + version: "14.0.0" extended_text_library: dependency: transitive description: @@ -408,10 +408,10 @@ packages: dependency: "direct main" description: name: ffi - sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878" + sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.3" ffigen: dependency: "direct dev" description: @@ -865,18 +865,18 @@ packages: dependency: transitive description: name: material_color_utilities - sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.8.0" + version: "0.11.1" meta: dependency: transitive description: name: meta - sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 + sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.15.0" mime: dependency: transitive description: @@ -1037,14 +1037,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.8" - pointycastle: - dependency: transitive - description: - name: pointycastle - sha256: "43ac87de6e10afabc85c445745a7b799e04de84cebaa4fd7bf55a5e1e9604d29" - url: "https://pub.dev" - source: hosted - version: "3.7.4" pool: dependency: transitive description: @@ -1325,10 +1317,11 @@ packages: uni_links: dependency: "direct main" description: - name: uni_links - sha256: "051098acfc9e26a9fde03b487bef5d3d228ca8f67693480c6f33fd4fbb8e2b6e" - url: "https://pub.dev" - source: hosted + path: uni_links + ref: f416118d843a7e9ed117c7bb7bdc2deda5a9e86f + resolved-ref: f416118d843a7e9ed117c7bb7bdc2deda5a9e86f + url: "https://github.com/rustdesk-org/uni_links" + source: git version: "0.5.1" uni_links_desktop: dependency: "direct main" @@ -1558,18 +1551,18 @@ packages: dependency: "direct main" description: name: win32 - sha256: "464f5674532865248444b4c3daca12bd9bf2d7c47f759ce2617986e7229494a8" + sha256: "68d1e89a91ed61ad9c370f9f8b6effed9ae5e0ede22a270bdfa6daf79fc2290a" url: "https://pub.dev" source: hosted - version: "5.2.0" + version: "5.5.4" win32_registry: dependency: transitive description: name: win32_registry - sha256: "41fd8a189940d8696b1b810efb9abcf60827b6cbfab90b0c43e8439e3a39d85a" + sha256: "21ec76dfc731550fd3e2ce7a33a9ea90b828fdf19a5c3bcf556fa992cfa99852" url: "https://pub.dev" source: hosted - version: "1.1.2" + version: "1.1.5" window_manager: dependency: "direct main" description: @@ -1629,5 +1622,5 @@ packages: source: hosted version: "0.2.1" sdks: - dart: ">=3.3.0 <4.0.0" - flutter: ">=3.19.0" + dart: ">=3.5.0 <4.0.0" + flutter: ">=3.24.0" diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index 0f105d702bf3..1855aebec007 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -78,7 +78,11 @@ dependencies: # if build rustdesk by flutter >=3.3, please use our custom pub below (uncomment code below). git: url: https://github.com/rustdesk-org/flutter_improved_scrolling - uni_links: ^0.5.1 + uni_links: + git: + url: https://github.com/rustdesk-org/uni_links + path: uni_links + ref: f416118d843a7e9ed117c7bb7bdc2deda5a9e86f uni_links_desktop: ^0.1.6 # use 0.1.6 to make flutter 3.13 works path: ^1.8.1 auto_size_text: ^3.0.0 @@ -104,7 +108,7 @@ dependencies: pull_down_button: ^0.9.3 device_info_plus: ^9.1.0 qr_flutter: ^4.1.0 - extended_text: 13.0.0 + extended_text: 14.0.0 dev_dependencies: icons_launcher: ^2.0.4 @@ -186,3 +190,4 @@ flutter: # # For details regarding fonts from package dependencies, # see https://flutter.dev/custom-fonts/#from-packages + From 912f5265f15d56361532241f21abfbfb084ea682 Mon Sep 17 00:00:00 2001 From: RustDesk <71636191+rustdesk@users.noreply.github.com> Date: Sat, 9 Nov 2024 23:32:18 +0800 Subject: [PATCH 366/541] Revert "Refact/flutter 3.24.4 (#9870)" (#9871) This reverts commit 5eb2c31207fdbc8d6bf868b187aef15d9dbf28d7. --- .github/workflows/bridge.yml | 37 +------ .github/workflows/flutter-build.yml | 103 ++++++++---------- flutter/android/app/build.gradle | 17 ++- flutter/android/build.gradle | 17 ++- .../gradle/wrapper/gradle-wrapper.properties | 2 +- flutter/android/settings.gradle | 30 ++--- .../lib/desktop/pages/connection_page.dart | 11 +- flutter/pubspec.lock | 49 +++++---- flutter/pubspec.yaml | 9 +- 9 files changed, 122 insertions(+), 153 deletions(-) diff --git a/.github/workflows/bridge.yml b/.github/workflows/bridge.yml index f06956c18330..54180ccdd345 100644 --- a/.github/workflows/bridge.yml +++ b/.github/workflows/bridge.yml @@ -6,7 +6,7 @@ on: workflow_call: env: - FLUTTER_VERSION: "3.22.3" + FLUTTER_VERSION: "3.19.6" FLUTTER_RUST_BRIDGE_VERSION: "1.80.1" RUST_VERSION: "1.75" # https://github.com/rustdesk/rustdesk/discussions/7503 @@ -22,18 +22,11 @@ jobs: os: ubuntu-20.04, extra-build-args: "", } - - { - target: aarch64-apple-darwin, - os: macos-latest, - arch: aarch64, - extra-build-args: "", - } steps: - name: Checkout source code uses: actions/checkout@v4 - name: Install prerequisites - if: matrix.job.os == 'ubuntu-20.04' run: | sudo apt-get install ca-certificates -y sudo apt-get update -y @@ -81,22 +74,13 @@ jobs: shell: bash run: | cargo install flutter_rust_bridge_codegen --version ${{ env.FLUTTER_RUST_BRIDGE_VERSION }} --features "uuid" - pushd flutter && sed -i -e 's/extended_text: 14.0.0/extended_text: 13.0.0/g' pubspec.yaml && flutter pub get && popd + pushd flutter && flutter pub get && popd - name: Run flutter rust bridge run: | - case ${{ matrix.job.os }} in - ubuntu-20.04) - ~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart - ;; - macos-latest) - ~/.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 - ~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart --c-output ./flutter/ios/Runner/bridge_generated.h - ;; - esac + ~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart - - name: Upload Artifact(ubuntu) - if: matrix.job.os == 'ubuntu-20.04' + - name: Upload Artifact uses: actions/upload-artifact@master with: name: bridge-artifact @@ -105,16 +89,3 @@ jobs: ./src/bridge_generated.io.rs ./flutter/lib/generated_bridge.dart ./flutter/lib/generated_bridge.freezed.dart - - - name: Upload Artifact(macos) - if: matrix.job.os == 'macos-latest' - uses: actions/upload-artifact@master - with: - name: bridge-artifact-macos - path: | - ./src/bridge_generated.rs - ./src/bridge_generated.io.rs - ./flutter/lib/generated_bridge.dart - ./flutter/lib/generated_bridge.freezed.dart - ./flutter/macos/Runner/bridge_generated.h - ./flutter/ios/Runner/bridge_generated.h diff --git a/.github/workflows/flutter-build.yml b/.github/workflows/flutter-build.yml index adfc14c86780..5c2c608a821d 100644 --- a/.github/workflows/flutter-build.yml +++ b/.github/workflows/flutter-build.yml @@ -24,8 +24,9 @@ env: SCITER_ARMV7_CMAKE_VERSION: "3.29.7" SCITER_NASM_DEBVERSION: "2.14-1" LLVM_VERSION: "15.0.6" - FLUTTER_VERSION: "3.24.4" - ANDROID_FLUTTER_VERSION: "3.24.4" + FLUTTER_VERSION: "3.19.6" + ANDROID_FLUTTER_VERSION: "3.13.9" # >= 3.16 is very slow on my android phone, but work well on most of others. We may switch to new flutter after changing to texture rendering (I believe it can solve my problem). + 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: "${{ inputs.upload-tag }}" @@ -45,9 +46,6 @@ env: SIGN_BASE_URL: "${{ secrets.SIGN_BASE_URL }}" jobs: - generate-bridge: - uses: ./.github/workflows/bridge.yml - build-RustDeskTempTopMostWindow: uses: ./.github/workflows/third-party-RustDeskTempTopMostWindow.yml with: @@ -61,7 +59,7 @@ jobs: build-for-windows-flutter: name: ${{ matrix.job.target }} - needs: [build-RustDeskTempTopMostWindow, generate-bridge] + needs: [build-RustDeskTempTopMostWindow] runs-on: ${{ matrix.job.os }} strategy: fail-fast: false @@ -87,12 +85,6 @@ jobs: - name: Checkout source code uses: actions/checkout@v4 - - name: Restore bridge files - uses: actions/download-artifact@master - with: - name: bridge-artifact - path: ./ - - name: Install LLVM and Clang uses: KyleMayes/install-llvm-action@v1 with: @@ -105,15 +97,6 @@ jobs: flutter-version: ${{ env.FLUTTER_VERSION }} cache: true - # https://github.com/flutter/flutter/issues/155685 - - name: Replace engine with rustdesk custom flutter engine - run: | - flutter doctor -v - flutter precache --windows - Invoke-WebRequest -Uri https://github.com/rustdesk/engine/releases/download/main/windows-x64-release.zip -OutFile windows-x64-release.zip - Expand-Archive -Path windows-x64-release.zip -DestinationPath windows-x64-release - mv -Force windows-x64-release/*  C:/hostedtoolcache/windows/flutter/stable-${{ env.FLUTTER_VERSION }}-x64/bin/cache/artifacts/engine/windows-x64-release/ - - name: Install Rust toolchain uses: dtolnay/rust-toolchain@v1 with: @@ -125,6 +108,13 @@ jobs: with: prefix-key: ${{ matrix.job.os }} + - 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" + 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: @@ -384,7 +374,6 @@ jobs: # use build-for-macOS instead if: false runs-on: [self-hosted, macOS, ARM64] - needs: [generate-bridge] steps: - name: Export GitHub Actions cache environment variables uses: actions/github-script@v6 @@ -396,11 +385,12 @@ jobs: - name: Checkout source code uses: actions/checkout@v4 - - name: Restore bridge files - uses: actions/download-artifact@master - with: - name: bridge-artifact-macos - path: ./ + - name: Install flutter rust bridge deps + shell: bash + run: | + cargo install flutter_rust_bridge_codegen --version ${{ env.FLUTTER_RUST_BRIDGE_VERSION }} --features "uuid" + 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: Build rustdesk run: | @@ -456,7 +446,6 @@ jobs: if: ${{ inputs.upload-artifact }} name: build rustdesk ios ipa runs-on: ${{ matrix.job.os }} - needs: [generate-bridge] strategy: fail-fast: false matrix: @@ -521,11 +510,12 @@ jobs: prefix-key: rustdesk-lib-cache-ios key: ${{ matrix.job.target }} - - name: Restore bridge files - uses: actions/download-artifact@master - with: - name: bridge-artifact-macos - path: ./ + - name: Install flutter rust bridge deps + shell: bash + run: | + cargo install flutter_rust_bridge_codegen --version ${{ env.FLUTTER_RUST_BRIDGE_VERSION }} --features "uuid" + 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/ios/Runner/bridge_generated.h - name: Build rustdesk lib run: | @@ -560,7 +550,6 @@ jobs: #if: ${{ inputs.upload-artifact }} if: false runs-on: [self-hosted, macOS, ARM64] - needs: [generate-bridge] strategy: fail-fast: false steps: @@ -576,11 +565,12 @@ jobs: # $VCPKG_ROOT/vcpkg install --triplet arm64-ios --x-install-root="$VCPKG_ROOT/installed" - - name: Restore bridge files - uses: actions/download-artifact@master - with: - name: bridge-artifact-macos - path: ./ + - name: Install flutter rust bridge deps + shell: bash + run: | + cargo install flutter_rust_bridge_codegen --version ${{ env.FLUTTER_RUST_BRIDGE_VERSION }} --features "uuid" + 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/ios/Runner/bridge_generated.h - name: Build rustdesk lib run: | @@ -615,7 +605,6 @@ jobs: build-for-macOS: name: ${{ matrix.job.target }} runs-on: ${{ matrix.job.os }} - needs: [generate-bridge] strategy: fail-fast: false matrix: @@ -706,11 +695,12 @@ jobs: with: prefix-key: ${{ matrix.job.os }} - - name: Restore bridge files - uses: actions/download-artifact@master - with: - name: bridge-artifact-macos - path: ./ + - name: Install flutter rust bridge deps + shell: bash + run: | + cargo install flutter_rust_bridge_codegen --version ${{ env.FLUTTER_RUST_BRIDGE_VERSION }} --features "uuid" + 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 @@ -832,8 +822,11 @@ jobs: tag_name: ${{ env.TAG_NAME }} files: rustdesk-${{ env.VERSION }}-unsigned.tar.gz + generate-bridge-linux: + uses: ./.github/workflows/bridge.yml + build-rustdesk-android: - needs: [generate-bridge] + needs: [generate-bridge-linux] name: build rustdesk android apk ${{ matrix.job.target }} runs-on: ${{ matrix.job.os }} strategy: @@ -910,7 +903,7 @@ jobs: llvm-10-dev \ nasm \ ninja-build \ - openjdk-17-jdk-headless \ + openjdk-11-jdk-headless \ pkg-config \ tree \ wget @@ -981,7 +974,7 @@ jobs: key: ${{ matrix.job.target }} - name: fix android for flutter 3.13 - if: ${{ env.ANDROID_FLUTTER_VERSION == '3.13.9' }} + if: $${{ env.ANDROID_FLUTTER_VERSION == '3.13.9' }} run: | cd flutter sed -i 's/uni_links_desktop/#uni_links_desktop/g' pubspec.yaml @@ -1029,9 +1022,9 @@ jobs: - name: Build rustdesk shell: bash env: - JAVA_HOME: /usr/lib/jvm/java-17-openjdk-amd64 + JAVA_HOME: /usr/lib/jvm/java-11-openjdk-amd64 run: | - export PATH=/usr/lib/jvm/java-17-openjdk-amd64/bin:$PATH + 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 @@ -1173,7 +1166,7 @@ jobs: llvm-10-dev \ nasm \ ninja-build \ - openjdk-17-jdk-headless \ + openjdk-11-jdk-headless \ pkg-config \ tree \ wget @@ -1218,7 +1211,7 @@ jobs: path: ./flutter/android/app/src/main/jniLibs/x86 - name: fix android for flutter 3.13 - if: ${{ env.ANDROID_FLUTTER_VERSION == '3.13.9' }} + if: $${{ env.ANDROID_FLUTTER_VERSION == '3.13.9' }} run: | cd flutter sed -i 's/uni_links_desktop/#uni_links_desktop/g' pubspec.yaml @@ -1230,9 +1223,9 @@ jobs: - name: Build rustdesk shell: bash env: - JAVA_HOME: /usr/lib/jvm/java-17-openjdk-amd64 + JAVA_HOME: /usr/lib/jvm/java-11-openjdk-amd64 run: | - export PATH=/usr/lib/jvm/java-17-openjdk-amd64/bin:$PATH + 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 mv ./flutter/android/app/src/main/jniLibs/arm64-v8a/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/arm64-v8a/librustdesk.so @@ -1292,7 +1285,7 @@ jobs: signed-apk/rustdesk-${{ env.VERSION }}-universal${{ env.suffix }}.apk build-rustdesk-linux: - needs: [generate-bridge] + needs: [generate-bridge-linux] name: build rustdesk linux ${{ matrix.job.target }} runs-on: ${{ matrix.job.on }} strategy: diff --git a/flutter/android/app/build.gradle b/flutter/android/app/build.gradle index c554251656a8..320eb3347c0e 100644 --- a/flutter/android/app/build.gradle +++ b/flutter/android/app/build.gradle @@ -1,9 +1,6 @@ import com.google.protobuf.gradle.* plugins { id "com.google.protobuf" version "0.9.4" - id "com.android.application" - id "kotlin-android" - id "dev.flutter.flutter-gradle-plugin" } def keystoreProperties = new Properties() @@ -20,6 +17,11 @@ if (localPropertiesFile.exists()) { } } +def flutterRoot = localProperties.getProperty('flutter.sdk') +if (flutterRoot == null) { + throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") +} + def flutterVersionCode = localProperties.getProperty('flutter.versionCode') if (flutterVersionCode == null) { flutterVersionCode = '1' @@ -30,6 +32,10 @@ if (flutterVersionName == null) { flutterVersionName = '1.0' } +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" + dependencies { implementation 'com.google.protobuf:protobuf-javalite:3.20.1' } @@ -51,7 +57,7 @@ protobuf { } android { - compileSdkVersion 34 + compileSdkVersion 33 sourceSets { main.java.srcDirs += 'src/main/kotlin' @@ -99,6 +105,7 @@ flutter { dependencies { implementation "androidx.media:media:1.6.0" implementation 'com.github.getActivity:XXPermissions:18.5' - implementation("org.jetbrains.kotlin:kotlin-stdlib") { version { strictly("1.9.10") } } + implementation("org.jetbrains.kotlin:kotlin-stdlib") { version { strictly("$kotlin_version") } } implementation 'com.caverock:androidsvg-aar:1.4' } + diff --git a/flutter/android/build.gradle b/flutter/android/build.gradle index 401bea0096e2..c6a77f36b171 100644 --- a/flutter/android/build.gradle +++ b/flutter/android/build.gradle @@ -1,3 +1,18 @@ +buildscript { + ext.kotlin_version = '1.9.10' + repositories { + google() + jcenter() + maven { url 'https://jitpack.io' } + } + + dependencies { + classpath 'com.android.tools.build:gradle:7.0.0' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + classpath 'com.google.gms:google-services:4.3.14' + } +} + allprojects { repositories { google() @@ -9,8 +24,6 @@ allprojects { rootProject.buildDir = '../build' subprojects { project.buildDir = "${rootProject.buildDir}/${project.name}" -} -subprojects { project.evaluationDependsOn(':app') } diff --git a/flutter/android/gradle/wrapper/gradle-wrapper.properties b/flutter/android/gradle/wrapper/gradle-wrapper.properties index cb576305fbe9..cc5527d781a7 100644 --- a/flutter/android/gradle/wrapper/gradle-wrapper.properties +++ b/flutter/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.4-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip diff --git a/flutter/android/settings.gradle b/flutter/android/settings.gradle index c5fb685a1614..44e62bcf06ae 100644 --- a/flutter/android/settings.gradle +++ b/flutter/android/settings.gradle @@ -1,25 +1,11 @@ -pluginManagement { - def flutterSdkPath = { - def properties = new Properties() - file("local.properties").withInputStream { properties.load(it) } - def flutterSdkPath = properties.getProperty("flutter.sdk") - assert flutterSdkPath != null, "flutter.sdk not set in local.properties" - return flutterSdkPath - }() +include ':app' - includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") +def localPropertiesFile = new File(rootProject.projectDir, "local.properties") +def properties = new Properties() - repositories { - google() - mavenCentral() - gradlePluginPortal() - } -} +assert localPropertiesFile.exists() +localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } -plugins { - id "dev.flutter.flutter-plugin-loader" version "1.0.0" - id "com.android.application" version "7.3.0" apply false - id "org.jetbrains.kotlin.android" version "1.9.10" apply false -} - -include ":app" +def flutterSdkPath = properties.getProperty("flutter.sdk") +assert flutterSdkPath != null, "flutter.sdk not set in local.properties" +apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index e2681bb377d1..744c05f9c223 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -203,8 +203,6 @@ class _ConnectionPageState extends State bool isPeersLoading = false; bool isPeersLoaded = false; - // https://github.com/flutter/flutter/issues/157244 - Iterable _autocompleteOpts = []; @override void initState() { @@ -332,7 +330,7 @@ class _ConnectionPageState extends State child: Autocomplete( optionsBuilder: (TextEditingValue textEditingValue) { if (textEditingValue.text == '') { - _autocompleteOpts = const Iterable.empty(); + return const Iterable.empty(); } else if (peers.isEmpty && !isPeersLoaded) { Peer emptyPeer = Peer( id: '', @@ -348,7 +346,7 @@ class _ConnectionPageState extends State rdpUsername: '', loginName: '', ); - _autocompleteOpts = [emptyPeer]; + return [emptyPeer]; } else { String textWithoutSpaces = textEditingValue.text.replaceAll(" ", ""); @@ -359,7 +357,8 @@ class _ConnectionPageState extends State ); } String textToFind = textEditingValue.text.toLowerCase(); - _autocompleteOpts = peers + + return peers .where((peer) => peer.id.toLowerCase().contains(textToFind) || peer.username @@ -371,7 +370,6 @@ class _ConnectionPageState extends State peer.alias.toLowerCase().contains(textToFind)) .toList(); } - return _autocompleteOpts; }, fieldViewBuilder: ( BuildContext context, @@ -432,7 +430,6 @@ class _ConnectionPageState extends State optionsViewBuilder: (BuildContext context, AutocompleteOnSelected onSelected, Iterable options) { - options = _autocompleteOpts; double maxHeight = options.length * 50; if (options.length == 1) { maxHeight = 52; diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index 2f408f4eb423..1a8badd89ae7 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -37,10 +37,10 @@ packages: dependency: transitive description: name: archive - sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d + sha256: "22600aa1e926be775fa5fe7e6894e7fb3df9efda8891c73f70fb3262399a432d" url: "https://pub.dev" source: hosted - version: "3.6.1" + version: "3.4.10" args: dependency: transitive description: @@ -384,10 +384,10 @@ packages: dependency: "direct main" description: name: extended_text - sha256: "38c1cac571d6eaf406f4b80040c1f88561e7617ad90795aac6a1be0a8d0bb676" + sha256: "7f382de3af12992e34bd72ddd36becf90c4720900af126cb9859f0189af71ffe" url: "https://pub.dev" source: hosted - version: "14.0.0" + version: "13.0.0" extended_text_library: dependency: transitive description: @@ -408,10 +408,10 @@ packages: dependency: "direct main" description: name: ffi - sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" + sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878" url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.1.0" ffigen: dependency: "direct dev" description: @@ -865,18 +865,18 @@ packages: dependency: transitive description: name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" url: "https://pub.dev" source: hosted - version: "0.11.1" + version: "0.8.0" meta: dependency: transitive description: name: meta - sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 + sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 url: "https://pub.dev" source: hosted - version: "1.15.0" + version: "1.11.0" mime: dependency: transitive description: @@ -1037,6 +1037,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.8" + pointycastle: + dependency: transitive + description: + name: pointycastle + sha256: "43ac87de6e10afabc85c445745a7b799e04de84cebaa4fd7bf55a5e1e9604d29" + url: "https://pub.dev" + source: hosted + version: "3.7.4" pool: dependency: transitive description: @@ -1317,11 +1325,10 @@ packages: uni_links: dependency: "direct main" description: - path: uni_links - ref: f416118d843a7e9ed117c7bb7bdc2deda5a9e86f - resolved-ref: f416118d843a7e9ed117c7bb7bdc2deda5a9e86f - url: "https://github.com/rustdesk-org/uni_links" - source: git + name: uni_links + sha256: "051098acfc9e26a9fde03b487bef5d3d228ca8f67693480c6f33fd4fbb8e2b6e" + url: "https://pub.dev" + source: hosted version: "0.5.1" uni_links_desktop: dependency: "direct main" @@ -1551,18 +1558,18 @@ packages: dependency: "direct main" description: name: win32 - sha256: "68d1e89a91ed61ad9c370f9f8b6effed9ae5e0ede22a270bdfa6daf79fc2290a" + sha256: "464f5674532865248444b4c3daca12bd9bf2d7c47f759ce2617986e7229494a8" url: "https://pub.dev" source: hosted - version: "5.5.4" + version: "5.2.0" win32_registry: dependency: transitive description: name: win32_registry - sha256: "21ec76dfc731550fd3e2ce7a33a9ea90b828fdf19a5c3bcf556fa992cfa99852" + sha256: "41fd8a189940d8696b1b810efb9abcf60827b6cbfab90b0c43e8439e3a39d85a" url: "https://pub.dev" source: hosted - version: "1.1.5" + version: "1.1.2" window_manager: dependency: "direct main" description: @@ -1622,5 +1629,5 @@ packages: source: hosted version: "0.2.1" sdks: - dart: ">=3.5.0 <4.0.0" - flutter: ">=3.24.0" + dart: ">=3.3.0 <4.0.0" + flutter: ">=3.19.0" diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index 1855aebec007..0f105d702bf3 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -78,11 +78,7 @@ dependencies: # if build rustdesk by flutter >=3.3, please use our custom pub below (uncomment code below). git: url: https://github.com/rustdesk-org/flutter_improved_scrolling - uni_links: - git: - url: https://github.com/rustdesk-org/uni_links - path: uni_links - ref: f416118d843a7e9ed117c7bb7bdc2deda5a9e86f + uni_links: ^0.5.1 uni_links_desktop: ^0.1.6 # use 0.1.6 to make flutter 3.13 works path: ^1.8.1 auto_size_text: ^3.0.0 @@ -108,7 +104,7 @@ dependencies: pull_down_button: ^0.9.3 device_info_plus: ^9.1.0 qr_flutter: ^4.1.0 - extended_text: 14.0.0 + extended_text: 13.0.0 dev_dependencies: icons_launcher: ^2.0.4 @@ -190,4 +186,3 @@ flutter: # # For details regarding fonts from package dependencies, # see https://flutter.dev/custom-fonts/#from-packages - From 72a1f1161ef64d1b039754f67435e58dd2ce785b Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Sun, 10 Nov 2024 11:18:08 +0800 Subject: [PATCH 367/541] refact: flutter 3.24.4 (#9874) Signed-off-by: fufesou --- .github/workflows/bridge.yml | 37 ++++- .github/workflows/flutter-build.yml | 127 +++++++++++------- flutter/android/app/build.gradle | 17 +-- flutter/android/build.gradle | 17 +-- .../gradle/wrapper/gradle-wrapper.properties | 2 +- flutter/android/settings.gradle | 30 +++-- .../lib/desktop/pages/connection_page.dart | 11 +- flutter/pubspec.lock | 49 +++---- flutter/pubspec.yaml | 9 +- 9 files changed, 173 insertions(+), 126 deletions(-) diff --git a/.github/workflows/bridge.yml b/.github/workflows/bridge.yml index 54180ccdd345..f06956c18330 100644 --- a/.github/workflows/bridge.yml +++ b/.github/workflows/bridge.yml @@ -6,7 +6,7 @@ on: workflow_call: env: - FLUTTER_VERSION: "3.19.6" + FLUTTER_VERSION: "3.22.3" FLUTTER_RUST_BRIDGE_VERSION: "1.80.1" RUST_VERSION: "1.75" # https://github.com/rustdesk/rustdesk/discussions/7503 @@ -22,11 +22,18 @@ jobs: os: ubuntu-20.04, extra-build-args: "", } + - { + target: aarch64-apple-darwin, + os: macos-latest, + arch: aarch64, + extra-build-args: "", + } steps: - name: Checkout source code uses: actions/checkout@v4 - name: Install prerequisites + if: matrix.job.os == 'ubuntu-20.04' run: | sudo apt-get install ca-certificates -y sudo apt-get update -y @@ -74,13 +81,22 @@ jobs: shell: bash run: | cargo install flutter_rust_bridge_codegen --version ${{ env.FLUTTER_RUST_BRIDGE_VERSION }} --features "uuid" - pushd flutter && flutter pub get && popd + pushd flutter && sed -i -e 's/extended_text: 14.0.0/extended_text: 13.0.0/g' pubspec.yaml && flutter pub get && popd - name: Run flutter rust bridge run: | - ~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart + case ${{ matrix.job.os }} in + ubuntu-20.04) + ~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart + ;; + macos-latest) + ~/.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 + ~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart --c-output ./flutter/ios/Runner/bridge_generated.h + ;; + esac - - name: Upload Artifact + - name: Upload Artifact(ubuntu) + if: matrix.job.os == 'ubuntu-20.04' uses: actions/upload-artifact@master with: name: bridge-artifact @@ -89,3 +105,16 @@ jobs: ./src/bridge_generated.io.rs ./flutter/lib/generated_bridge.dart ./flutter/lib/generated_bridge.freezed.dart + + - name: Upload Artifact(macos) + if: matrix.job.os == 'macos-latest' + uses: actions/upload-artifact@master + with: + name: bridge-artifact-macos + path: | + ./src/bridge_generated.rs + ./src/bridge_generated.io.rs + ./flutter/lib/generated_bridge.dart + ./flutter/lib/generated_bridge.freezed.dart + ./flutter/macos/Runner/bridge_generated.h + ./flutter/ios/Runner/bridge_generated.h diff --git a/.github/workflows/flutter-build.yml b/.github/workflows/flutter-build.yml index 5c2c608a821d..6fd234969777 100644 --- a/.github/workflows/flutter-build.yml +++ b/.github/workflows/flutter-build.yml @@ -24,9 +24,8 @@ env: SCITER_ARMV7_CMAKE_VERSION: "3.29.7" SCITER_NASM_DEBVERSION: "2.14-1" LLVM_VERSION: "15.0.6" - FLUTTER_VERSION: "3.19.6" - ANDROID_FLUTTER_VERSION: "3.13.9" # >= 3.16 is very slow on my android phone, but work well on most of others. We may switch to new flutter after changing to texture rendering (I believe it can solve my problem). - FLUTTER_RUST_BRIDGE_VERSION: "1.80.1" + FLUTTER_VERSION: "3.24.4" + ANDROID_FLUTTER_VERSION: "3.24.4" # for arm64 linux because official Dart SDK does not work FLUTTER_ELINUX_VERSION: "3.16.9" TAG_NAME: "${{ inputs.upload-tag }}" @@ -46,6 +45,9 @@ env: SIGN_BASE_URL: "${{ secrets.SIGN_BASE_URL }}" jobs: + generate-bridge: + uses: ./.github/workflows/bridge.yml + build-RustDeskTempTopMostWindow: uses: ./.github/workflows/third-party-RustDeskTempTopMostWindow.yml with: @@ -59,7 +61,7 @@ jobs: build-for-windows-flutter: name: ${{ matrix.job.target }} - needs: [build-RustDeskTempTopMostWindow] + needs: [build-RustDeskTempTopMostWindow, generate-bridge] runs-on: ${{ matrix.job.os }} strategy: fail-fast: false @@ -85,6 +87,12 @@ jobs: - name: Checkout source code uses: actions/checkout@v4 + - name: Restore bridge files + uses: actions/download-artifact@master + with: + name: bridge-artifact + path: ./ + - name: Install LLVM and Clang uses: KyleMayes/install-llvm-action@v1 with: @@ -97,6 +105,15 @@ jobs: flutter-version: ${{ env.FLUTTER_VERSION }} cache: true + # https://github.com/flutter/flutter/issues/155685 + - name: Replace engine with rustdesk custom flutter engine + run: | + flutter doctor -v + flutter precache --windows + Invoke-WebRequest -Uri https://github.com/rustdesk/engine/releases/download/main/windows-x64-release.zip -OutFile windows-x64-release.zip + Expand-Archive -Path windows-x64-release.zip -DestinationPath windows-x64-release + mv -Force windows-x64-release/*  C:/hostedtoolcache/windows/flutter/stable-${{ env.FLUTTER_VERSION }}-x64/bin/cache/artifacts/engine/windows-x64-release/ + - name: Install Rust toolchain uses: dtolnay/rust-toolchain@v1 with: @@ -108,13 +125,6 @@ jobs: with: prefix-key: ${{ matrix.job.os }} - - 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" - 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: @@ -374,6 +384,7 @@ jobs: # use build-for-macOS instead if: false runs-on: [self-hosted, macOS, ARM64] + needs: [generate-bridge] steps: - name: Export GitHub Actions cache environment variables uses: actions/github-script@v6 @@ -385,12 +396,11 @@ jobs: - name: Checkout source code uses: actions/checkout@v4 - - name: Install flutter rust bridge deps - shell: bash - run: | - cargo install flutter_rust_bridge_codegen --version ${{ env.FLUTTER_RUST_BRIDGE_VERSION }} --features "uuid" - 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: Restore bridge files + uses: actions/download-artifact@master + with: + name: bridge-artifact-macos + path: ./ - name: Build rustdesk run: | @@ -446,6 +456,7 @@ jobs: if: ${{ inputs.upload-artifact }} name: build rustdesk ios ipa runs-on: ${{ matrix.job.os }} + needs: [generate-bridge] strategy: fail-fast: false matrix: @@ -510,12 +521,11 @@ jobs: prefix-key: rustdesk-lib-cache-ios key: ${{ matrix.job.target }} - - name: Install flutter rust bridge deps - shell: bash - run: | - cargo install flutter_rust_bridge_codegen --version ${{ env.FLUTTER_RUST_BRIDGE_VERSION }} --features "uuid" - 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/ios/Runner/bridge_generated.h + - name: Restore bridge files + uses: actions/download-artifact@master + with: + name: bridge-artifact-macos + path: ./ - name: Build rustdesk lib run: | @@ -550,6 +560,7 @@ jobs: #if: ${{ inputs.upload-artifact }} if: false runs-on: [self-hosted, macOS, ARM64] + needs: [generate-bridge] strategy: fail-fast: false steps: @@ -565,12 +576,11 @@ jobs: # $VCPKG_ROOT/vcpkg install --triplet arm64-ios --x-install-root="$VCPKG_ROOT/installed" - - name: Install flutter rust bridge deps - shell: bash - run: | - cargo install flutter_rust_bridge_codegen --version ${{ env.FLUTTER_RUST_BRIDGE_VERSION }} --features "uuid" - 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/ios/Runner/bridge_generated.h + - name: Restore bridge files + uses: actions/download-artifact@master + with: + name: bridge-artifact-macos + path: ./ - name: Build rustdesk lib run: | @@ -605,6 +615,7 @@ jobs: build-for-macOS: name: ${{ matrix.job.target }} runs-on: ${{ matrix.job.os }} + needs: [generate-bridge] strategy: fail-fast: false matrix: @@ -695,12 +706,11 @@ jobs: with: prefix-key: ${{ matrix.job.os }} - - name: Install flutter rust bridge deps - shell: bash - run: | - cargo install flutter_rust_bridge_codegen --version ${{ env.FLUTTER_RUST_BRIDGE_VERSION }} --features "uuid" - 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: Restore bridge files + uses: actions/download-artifact@master + with: + name: bridge-artifact-macos + path: ./ - name: Setup vcpkg with Github Actions binary cache uses: lukka/run-vcpkg@v11 @@ -822,11 +832,8 @@ jobs: tag_name: ${{ env.TAG_NAME }} files: rustdesk-${{ env.VERSION }}-unsigned.tar.gz - generate-bridge-linux: - uses: ./.github/workflows/bridge.yml - build-rustdesk-android: - needs: [generate-bridge-linux] + needs: [generate-bridge] name: build rustdesk android apk ${{ matrix.job.target }} runs-on: ${{ matrix.job.os }} strategy: @@ -903,7 +910,7 @@ jobs: llvm-10-dev \ nasm \ ninja-build \ - openjdk-11-jdk-headless \ + openjdk-17-jdk-headless \ pkg-config \ tree \ wget @@ -974,7 +981,7 @@ jobs: key: ${{ matrix.job.target }} - name: fix android for flutter 3.13 - if: $${{ env.ANDROID_FLUTTER_VERSION == '3.13.9' }} + if: ${{ env.ANDROID_FLUTTER_VERSION == '3.13.9' }} run: | cd flutter sed -i 's/uni_links_desktop/#uni_links_desktop/g' pubspec.yaml @@ -1022,9 +1029,9 @@ jobs: - name: Build rustdesk shell: bash env: - JAVA_HOME: /usr/lib/jvm/java-11-openjdk-amd64 + JAVA_HOME: /usr/lib/jvm/java-17-openjdk-amd64 run: | - export PATH=/usr/lib/jvm/java-11-openjdk-amd64/bin:$PATH + export PATH=/usr/lib/jvm/java-17-openjdk-amd64/bin:$PATH # temporary use debug sign config sed -i "s/signingConfigs.release/signingConfigs.debug/g" ./flutter/android/app/build.gradle case ${{ matrix.job.target }} in @@ -1069,6 +1076,14 @@ jobs: mkdir -p signed-apk; pushd signed-apk mv ../rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}${{ matrix.job.suffix }}.apk . + # https://github.com/r0adkll/sign-android-release/issues/84#issuecomment-1889636075 + - name: Setup sign tool version variable + shell: bash + run: | + BUILD_TOOL_VERSION=$(ls /usr/local/lib/android/sdk/build-tools/ | tail -n 1) + echo "ANDROID_SIGN_TOOL_VERSION=$BUILD_TOOL_VERSION" >> $GITHUB_ENV + echo Last build tool version is: $BUILD_TOOL_VERSION + - uses: r0adkll/sign-android-release@v1 name: Sign app APK if: env.ANDROID_SIGNING_KEY != null @@ -1080,8 +1095,8 @@ jobs: 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" + # env.ANDROID_SIGN_TOOL_VERSION is set by Step "Setup sign tool version variable" + BUILD_TOOLS_VERSION: ${{ env.ANDROID_SIGN_TOOL_VERSION }} - name: Upload Artifacts if: env.ANDROID_SIGNING_KEY != null && env.UPLOAD_ARTIFACT == 'true' @@ -1166,7 +1181,7 @@ jobs: llvm-10-dev \ nasm \ ninja-build \ - openjdk-11-jdk-headless \ + openjdk-17-jdk-headless \ pkg-config \ tree \ wget @@ -1211,7 +1226,7 @@ jobs: path: ./flutter/android/app/src/main/jniLibs/x86 - name: fix android for flutter 3.13 - if: $${{ env.ANDROID_FLUTTER_VERSION == '3.13.9' }} + if: ${{ env.ANDROID_FLUTTER_VERSION == '3.13.9' }} run: | cd flutter sed -i 's/uni_links_desktop/#uni_links_desktop/g' pubspec.yaml @@ -1223,9 +1238,9 @@ jobs: - name: Build rustdesk shell: bash env: - JAVA_HOME: /usr/lib/jvm/java-11-openjdk-amd64 + JAVA_HOME: /usr/lib/jvm/java-17-openjdk-amd64 run: | - export PATH=/usr/lib/jvm/java-11-openjdk-amd64/bin:$PATH + export PATH=/usr/lib/jvm/java-17-openjdk-amd64/bin:$PATH # temporary use debug sign config sed -i "s/signingConfigs.release/signingConfigs.debug/g" ./flutter/android/app/build.gradle mv ./flutter/android/app/src/main/jniLibs/arm64-v8a/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/arm64-v8a/librustdesk.so @@ -1245,6 +1260,14 @@ jobs: mkdir -p signed-apk mv ./flutter/build/app/outputs/flutter-apk/app-${{ env.reltype }}.apk signed-apk/rustdesk-${{ env.VERSION }}-universal${{ env.suffix }}.apk + # https://github.com/r0adkll/sign-android-release/issues/84#issuecomment-1889636075 + - name: Setup sign tool version variable + shell: bash + run: | + BUILD_TOOL_VERSION=$(ls /usr/local/lib/android/sdk/build-tools/ | tail -n 1) + echo "ANDROID_SIGN_TOOL_VERSION=$BUILD_TOOL_VERSION" >> $GITHUB_ENV + echo Last build tool version is: $BUILD_TOOL_VERSION + - uses: r0adkll/sign-android-release@v1 name: Sign app APK if: env.ANDROID_SIGNING_KEY != null @@ -1256,8 +1279,8 @@ jobs: 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" + # env.ANDROID_SIGN_TOOL_VERSION is set by Step "Setup sign tool version variable" + BUILD_TOOLS_VERSION: ${{ env.ANDROID_SIGN_TOOL_VERSION }} - name: Upload Artifacts if: env.ANDROID_SIGNING_KEY != null && env.UPLOAD_ARTIFACT == 'true' @@ -1285,7 +1308,7 @@ jobs: signed-apk/rustdesk-${{ env.VERSION }}-universal${{ env.suffix }}.apk build-rustdesk-linux: - needs: [generate-bridge-linux] + needs: [generate-bridge] name: build rustdesk linux ${{ matrix.job.target }} runs-on: ${{ matrix.job.on }} strategy: diff --git a/flutter/android/app/build.gradle b/flutter/android/app/build.gradle index 320eb3347c0e..c554251656a8 100644 --- a/flutter/android/app/build.gradle +++ b/flutter/android/app/build.gradle @@ -1,6 +1,9 @@ import com.google.protobuf.gradle.* plugins { id "com.google.protobuf" version "0.9.4" + id "com.android.application" + id "kotlin-android" + id "dev.flutter.flutter-gradle-plugin" } def keystoreProperties = new Properties() @@ -17,11 +20,6 @@ if (localPropertiesFile.exists()) { } } -def flutterRoot = localProperties.getProperty('flutter.sdk') -if (flutterRoot == null) { - throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") -} - def flutterVersionCode = localProperties.getProperty('flutter.versionCode') if (flutterVersionCode == null) { flutterVersionCode = '1' @@ -32,10 +30,6 @@ if (flutterVersionName == null) { flutterVersionName = '1.0' } -apply plugin: 'com.android.application' -apply plugin: 'kotlin-android' -apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" - dependencies { implementation 'com.google.protobuf:protobuf-javalite:3.20.1' } @@ -57,7 +51,7 @@ protobuf { } android { - compileSdkVersion 33 + compileSdkVersion 34 sourceSets { main.java.srcDirs += 'src/main/kotlin' @@ -105,7 +99,6 @@ flutter { dependencies { implementation "androidx.media:media:1.6.0" 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("1.9.10") } } implementation 'com.caverock:androidsvg-aar:1.4' } - diff --git a/flutter/android/build.gradle b/flutter/android/build.gradle index c6a77f36b171..401bea0096e2 100644 --- a/flutter/android/build.gradle +++ b/flutter/android/build.gradle @@ -1,18 +1,3 @@ -buildscript { - ext.kotlin_version = '1.9.10' - repositories { - google() - jcenter() - maven { url 'https://jitpack.io' } - } - - dependencies { - classpath 'com.android.tools.build:gradle:7.0.0' - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - classpath 'com.google.gms:google-services:4.3.14' - } -} - allprojects { repositories { google() @@ -24,6 +9,8 @@ allprojects { rootProject.buildDir = '../build' subprojects { project.buildDir = "${rootProject.buildDir}/${project.name}" +} +subprojects { project.evaluationDependsOn(':app') } diff --git a/flutter/android/gradle/wrapper/gradle-wrapper.properties b/flutter/android/gradle/wrapper/gradle-wrapper.properties index cc5527d781a7..cb576305fbe9 100644 --- a/flutter/android/gradle/wrapper/gradle-wrapper.properties +++ b/flutter/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.4-all.zip diff --git a/flutter/android/settings.gradle b/flutter/android/settings.gradle index 44e62bcf06ae..c5fb685a1614 100644 --- a/flutter/android/settings.gradle +++ b/flutter/android/settings.gradle @@ -1,11 +1,25 @@ -include ':app' +pluginManagement { + def flutterSdkPath = { + def properties = new Properties() + file("local.properties").withInputStream { properties.load(it) } + def flutterSdkPath = properties.getProperty("flutter.sdk") + assert flutterSdkPath != null, "flutter.sdk not set in local.properties" + return flutterSdkPath + }() -def localPropertiesFile = new File(rootProject.projectDir, "local.properties") -def properties = new Properties() + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") -assert localPropertiesFile.exists() -localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} -def flutterSdkPath = properties.getProperty("flutter.sdk") -assert flutterSdkPath != null, "flutter.sdk not set in local.properties" -apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" +plugins { + id "dev.flutter.flutter-plugin-loader" version "1.0.0" + id "com.android.application" version "7.3.0" apply false + id "org.jetbrains.kotlin.android" version "1.9.10" apply false +} + +include ":app" diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index 744c05f9c223..e2681bb377d1 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -203,6 +203,8 @@ class _ConnectionPageState extends State bool isPeersLoading = false; bool isPeersLoaded = false; + // https://github.com/flutter/flutter/issues/157244 + Iterable _autocompleteOpts = []; @override void initState() { @@ -330,7 +332,7 @@ class _ConnectionPageState extends State child: Autocomplete( optionsBuilder: (TextEditingValue textEditingValue) { if (textEditingValue.text == '') { - return const Iterable.empty(); + _autocompleteOpts = const Iterable.empty(); } else if (peers.isEmpty && !isPeersLoaded) { Peer emptyPeer = Peer( id: '', @@ -346,7 +348,7 @@ class _ConnectionPageState extends State rdpUsername: '', loginName: '', ); - return [emptyPeer]; + _autocompleteOpts = [emptyPeer]; } else { String textWithoutSpaces = textEditingValue.text.replaceAll(" ", ""); @@ -357,8 +359,7 @@ class _ConnectionPageState extends State ); } String textToFind = textEditingValue.text.toLowerCase(); - - return peers + _autocompleteOpts = peers .where((peer) => peer.id.toLowerCase().contains(textToFind) || peer.username @@ -370,6 +371,7 @@ class _ConnectionPageState extends State peer.alias.toLowerCase().contains(textToFind)) .toList(); } + return _autocompleteOpts; }, fieldViewBuilder: ( BuildContext context, @@ -430,6 +432,7 @@ class _ConnectionPageState extends State optionsViewBuilder: (BuildContext context, AutocompleteOnSelected onSelected, Iterable options) { + options = _autocompleteOpts; double maxHeight = options.length * 50; if (options.length == 1) { maxHeight = 52; diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index 1a8badd89ae7..2f408f4eb423 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -37,10 +37,10 @@ packages: dependency: transitive description: name: archive - sha256: "22600aa1e926be775fa5fe7e6894e7fb3df9efda8891c73f70fb3262399a432d" + sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d url: "https://pub.dev" source: hosted - version: "3.4.10" + version: "3.6.1" args: dependency: transitive description: @@ -384,10 +384,10 @@ packages: dependency: "direct main" description: name: extended_text - sha256: "7f382de3af12992e34bd72ddd36becf90c4720900af126cb9859f0189af71ffe" + sha256: "38c1cac571d6eaf406f4b80040c1f88561e7617ad90795aac6a1be0a8d0bb676" url: "https://pub.dev" source: hosted - version: "13.0.0" + version: "14.0.0" extended_text_library: dependency: transitive description: @@ -408,10 +408,10 @@ packages: dependency: "direct main" description: name: ffi - sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878" + sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.3" ffigen: dependency: "direct dev" description: @@ -865,18 +865,18 @@ packages: dependency: transitive description: name: material_color_utilities - sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.8.0" + version: "0.11.1" meta: dependency: transitive description: name: meta - sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 + sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.15.0" mime: dependency: transitive description: @@ -1037,14 +1037,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.8" - pointycastle: - dependency: transitive - description: - name: pointycastle - sha256: "43ac87de6e10afabc85c445745a7b799e04de84cebaa4fd7bf55a5e1e9604d29" - url: "https://pub.dev" - source: hosted - version: "3.7.4" pool: dependency: transitive description: @@ -1325,10 +1317,11 @@ packages: uni_links: dependency: "direct main" description: - name: uni_links - sha256: "051098acfc9e26a9fde03b487bef5d3d228ca8f67693480c6f33fd4fbb8e2b6e" - url: "https://pub.dev" - source: hosted + path: uni_links + ref: f416118d843a7e9ed117c7bb7bdc2deda5a9e86f + resolved-ref: f416118d843a7e9ed117c7bb7bdc2deda5a9e86f + url: "https://github.com/rustdesk-org/uni_links" + source: git version: "0.5.1" uni_links_desktop: dependency: "direct main" @@ -1558,18 +1551,18 @@ packages: dependency: "direct main" description: name: win32 - sha256: "464f5674532865248444b4c3daca12bd9bf2d7c47f759ce2617986e7229494a8" + sha256: "68d1e89a91ed61ad9c370f9f8b6effed9ae5e0ede22a270bdfa6daf79fc2290a" url: "https://pub.dev" source: hosted - version: "5.2.0" + version: "5.5.4" win32_registry: dependency: transitive description: name: win32_registry - sha256: "41fd8a189940d8696b1b810efb9abcf60827b6cbfab90b0c43e8439e3a39d85a" + sha256: "21ec76dfc731550fd3e2ce7a33a9ea90b828fdf19a5c3bcf556fa992cfa99852" url: "https://pub.dev" source: hosted - version: "1.1.2" + version: "1.1.5" window_manager: dependency: "direct main" description: @@ -1629,5 +1622,5 @@ packages: source: hosted version: "0.2.1" sdks: - dart: ">=3.3.0 <4.0.0" - flutter: ">=3.19.0" + dart: ">=3.5.0 <4.0.0" + flutter: ">=3.24.0" diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index 0f105d702bf3..1855aebec007 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -78,7 +78,11 @@ dependencies: # if build rustdesk by flutter >=3.3, please use our custom pub below (uncomment code below). git: url: https://github.com/rustdesk-org/flutter_improved_scrolling - uni_links: ^0.5.1 + uni_links: + git: + url: https://github.com/rustdesk-org/uni_links + path: uni_links + ref: f416118d843a7e9ed117c7bb7bdc2deda5a9e86f uni_links_desktop: ^0.1.6 # use 0.1.6 to make flutter 3.13 works path: ^1.8.1 auto_size_text: ^3.0.0 @@ -104,7 +108,7 @@ dependencies: pull_down_button: ^0.9.3 device_info_plus: ^9.1.0 qr_flutter: ^4.1.0 - extended_text: 13.0.0 + extended_text: 14.0.0 dev_dependencies: icons_launcher: ^2.0.4 @@ -186,3 +190,4 @@ flutter: # # For details regarding fonts from package dependencies, # see https://flutter.dev/custom-fonts/#from-packages + From 4e6a43288e8d003488ff7e7b015ee4e95a42a4d6 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Sun, 10 Nov 2024 23:56:55 +0800 Subject: [PATCH 368/541] https://github.com/rustdesk/rustdesk/issues/9877 https://developer.apple.com/documentation/security/ksecusedataprotectionkeychain --- flutter/ios/Flutter/AppFrameworkInfo.plist | 2 +- flutter/ios/Podfile | 5 +---- flutter/ios/Podfile.lock | 2 +- flutter/ios/Runner.xcodeproj/project.pbxproj | 6 +++--- 4 files changed, 6 insertions(+), 9 deletions(-) diff --git a/flutter/ios/Flutter/AppFrameworkInfo.plist b/flutter/ios/Flutter/AppFrameworkInfo.plist index 7c5696400627..1dc6cf7652ba 100644 --- a/flutter/ios/Flutter/AppFrameworkInfo.plist +++ b/flutter/ios/Flutter/AppFrameworkInfo.plist @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 12.0 + 13.0 diff --git a/flutter/ios/Podfile b/flutter/ios/Podfile index 321d7132cfb3..b71c436f2291 100644 --- a/flutter/ios/Podfile +++ b/flutter/ios/Podfile @@ -1,10 +1,7 @@ -# Uncomment this line to define a global platform for your project -# platform :ios, '12.0' - # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' -platform :ios, '12.0' +platform :ios, '13.0' project 'Runner', { 'Debug' => :debug, diff --git a/flutter/ios/Podfile.lock b/flutter/ios/Podfile.lock index 6cb5c9cff4c5..b03cb7466fd8 100644 --- a/flutter/ios/Podfile.lock +++ b/flutter/ios/Podfile.lock @@ -137,6 +137,6 @@ SPEC CHECKSUMS: video_player_avfoundation: 02011213dab73ae3687df27ce441fbbcc82b5579 wakelock_plus: 8b09852c8876491e4b6d179e17dfe2a0b5f60d47 -PODFILE CHECKSUM: d4cb12ad5d3bdb3352770b1d3db237584e155156 +PODFILE CHECKSUM: 83d1b0fb6fc8613d8312a03b8e1540d37cfc5d2c COCOAPODS: 1.15.2 diff --git a/flutter/ios/Runner.xcodeproj/project.pbxproj b/flutter/ios/Runner.xcodeproj/project.pbxproj index acc2a09e7475..36dc89ea8a96 100644 --- a/flutter/ios/Runner.xcodeproj/project.pbxproj +++ b/flutter/ios/Runner.xcodeproj/project.pbxproj @@ -347,7 +347,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -491,7 +491,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -541,7 +541,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; From 6082bb275440b3b2c5d0d5adeac389d517364c68 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Mon, 11 Nov 2024 00:35:41 +0800 Subject: [PATCH 369/541] fix: save load window rect, Windows, ignore dpi (#9875) Signed-off-by: fufesou --- flutter/lib/common.dart | 88 ++++++++++++++++++----------------------- flutter/pubspec.lock | 2 +- 2 files changed, 40 insertions(+), 50 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index a2ad96775460..2b3bcd83cf2d 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -64,6 +64,9 @@ int androidVersion = 0; // So we need to use this flag to enable/disable resizable. bool _linuxWindowResizable = true; +// Only used on Windows(window manager). +bool _ignoreDevicePixelRatio = true; + /// only available for Windows target int windowsBuildNumber = 0; DesktopType? desktopType; @@ -1611,12 +1614,6 @@ Widget getPlatformImage(String platform, {double size = 50}) { return SvgPicture.asset('assets/$platform.svg', height: size, width: size); } -class OffsetDevicePixelRatio { - Offset offset; - final double devicePixelRatio; - OffsetDevicePixelRatio(this.offset, this.devicePixelRatio); -} - class LastWindowPosition { double? width; double? height; @@ -1699,8 +1696,10 @@ Future saveWindowPosition(WindowType type, {int? windowId}) async { if (isFullscreen || isMaximized) { setPreFrame(); } else { - position = await windowManager.getPosition(); - sz = await windowManager.getSize(); + position = await windowManager.getPosition( + ignoreDevicePixelRatio: _ignoreDevicePixelRatio); + sz = await windowManager.getSize( + ignoreDevicePixelRatio: _ignoreDevicePixelRatio); } break; default: @@ -1818,7 +1817,7 @@ bool isPointInRect(Offset point, Rect rect) { } /// return null means center -Future _adjustRestoreMainWindowOffset( +Future _adjustRestoreMainWindowOffset( double? left, double? top, double? width, @@ -1832,13 +1831,9 @@ Future _adjustRestoreMainWindowOffset( double? frameTop; double? frameRight; double? frameBottom; - double devicePixelRatio = 1.0; if (isDesktop || isWebDesktop) { for (final screen in await window_size.getScreenList()) { - if (isPointInRect(Offset(left, top), screen.visibleFrame)) { - devicePixelRatio = screen.scaleFactor; - } frameLeft = frameLeft == null ? screen.visibleFrame.left : min(screen.visibleFrame.left, frameLeft); @@ -1872,7 +1867,7 @@ Future _adjustRestoreMainWindowOffset( top < frameTop!) { return null; } else { - return OffsetDevicePixelRatio(Offset(left, top), devicePixelRatio); + return Offset(left, top); } } @@ -1932,47 +1927,23 @@ Future restoreWindowPosition(WindowType type, } final size = await _adjustRestoreMainWindowSize(lpos.width, lpos.height); - final offsetDevicePixelRatio = await _adjustRestoreMainWindowOffset( + final offsetLeftTop = await _adjustRestoreMainWindowOffset( lpos.offsetWidth, lpos.offsetHeight, size.width, size.height, ); debugPrint( - "restore lpos: ${size.width}/${size.height}, offset:${offsetDevicePixelRatio?.offset.dx}/${offsetDevicePixelRatio?.offset.dy}, devicePixelRatio:${offsetDevicePixelRatio?.devicePixelRatio}, isMaximized: ${lpos.isMaximized}, isFullscreen: ${lpos.isFullscreen}"); + "restore lpos: ${size.width}/${size.height}, offset:${offsetLeftTop?.dx}/${offsetLeftTop?.dy}, isMaximized: ${lpos.isMaximized}, isFullscreen: ${lpos.isFullscreen}"); switch (type) { case WindowType.Main: - // https://github.com/rustdesk/rustdesk/issues/8038 - // `setBounds()` in `window_manager` will use the current devicePixelRatio. - // So we need to adjust the offset by the scale factor. - // https://github.com/rustdesk-org/window_manager/blob/f19acdb008645366339444a359a45c3257c8b32e/windows/window_manager.cpp#L701 - if (isWindows) { - double? curDevicePixelRatio; - Offset curPos = await windowManager.getPosition(); - for (final screen in await window_size.getScreenList()) { - if (isPointInRect(curPos, screen.visibleFrame)) { - curDevicePixelRatio = screen.scaleFactor; - } - } - if (curDevicePixelRatio != null && - curDevicePixelRatio != 0 && - offsetDevicePixelRatio != null) { - if (offsetDevicePixelRatio.devicePixelRatio != 0) { - final scale = - offsetDevicePixelRatio.devicePixelRatio / curDevicePixelRatio; - offsetDevicePixelRatio.offset = - offsetDevicePixelRatio.offset.scale(scale, scale); - debugPrint( - "restore new offset: ${offsetDevicePixelRatio.offset.dx}/${offsetDevicePixelRatio.offset.dy}, scale:$scale"); - } - } - } restorePos() async { - if (offsetDevicePixelRatio == null) { + if (offsetLeftTop == null) { await windowManager.center(); } else { - await windowManager.setPosition(offsetDevicePixelRatio.offset); + await windowManager.setPosition(offsetLeftTop, + ignoreDevicePixelRatio: _ignoreDevicePixelRatio); } } if (lpos.isMaximized == true) { @@ -1981,20 +1952,39 @@ Future restoreWindowPosition(WindowType type, await windowManager.maximize(); } } else { - if (!bind.isIncomingOnly() || bind.isOutgoingOnly()) { - await windowManager.setSize(size); + final storeSize = !bind.isIncomingOnly() || bind.isOutgoingOnly(); + if (isWindows) { + if (storeSize) { + // We need to set the window size first to avoid the incorrect size in some special cases. + // E.g. There are two monitors, the left one is 100% DPI and the right one is 175% DPI. + // The window belongs to the left monitor, but if it is moved a little to the right, it will belong to the right monitor. + // After restoring, the size will be incorrect. + // See known issue in https://github.com/rustdesk/rustdesk/pull/9840 + await windowManager.setSize(size, + ignoreDevicePixelRatio: _ignoreDevicePixelRatio); + } + await restorePos(); + if (storeSize) { + await windowManager.setSize(size, + ignoreDevicePixelRatio: _ignoreDevicePixelRatio); + } + } else { + if (storeSize) { + await windowManager.setSize(size, + ignoreDevicePixelRatio: _ignoreDevicePixelRatio); + } + await restorePos(); } - await restorePos(); } return true; default: final wc = WindowController.fromWindowId(windowId!); restoreFrame() async { - if (offsetDevicePixelRatio == null) { + if (offsetLeftTop == null) { await wc.center(); } else { - final frame = Rect.fromLTWH(offsetDevicePixelRatio.offset.dx, - offsetDevicePixelRatio.offset.dy, size.width, size.height); + final frame = Rect.fromLTWH( + offsetLeftTop.dx, offsetLeftTop.dy, size.width, size.height); await wc.setFrame(frame); } } diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index 2f408f4eb423..4010edf974d1 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -1568,7 +1568,7 @@ packages: description: path: "." ref: HEAD - resolved-ref: f19acdb008645366339444a359a45c3257c8b32e + resolved-ref: "85789bfe6e4cfaf4ecc00c52857467fdb7f26879" url: "https://github.com/rustdesk-org/window_manager" source: git version: "0.3.6" From a79a9f697b5a92f593f8c27edf7825f6a53a6fca Mon Sep 17 00:00:00 2001 From: 21pages Date: Mon, 11 Nov 2024 08:39:54 +0800 Subject: [PATCH 370/541] fix "Add to addresssBook" dropdown menu (#9878) Signed-off-by: 21pages --- ...ter_3.24.4_dropdown_menu_enableFilter.diff | 42 +++++++++++++++++++ .github/workflows/flutter-build.yml | 42 +++++++++++++++++++ 2 files changed, 84 insertions(+) create mode 100644 .github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff diff --git a/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff b/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff new file mode 100644 index 000000000000..9b8ea26906e9 --- /dev/null +++ b/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff @@ -0,0 +1,42 @@ +diff --git a/packages/flutter/lib/src/material/dropdown_menu.dart b/packages/flutter/lib/src/material/dropdown_menu.dart +index 7e634cd2aa..c1e9acc295 100644 +--- a/packages/flutter/lib/src/material/dropdown_menu.dart ++++ b/packages/flutter/lib/src/material/dropdown_menu.dart +@@ -475,7 +475,7 @@ class _DropdownMenuState extends State> { + final GlobalKey _leadingKey = GlobalKey(); + late List buttonItemKeys; + final MenuController _controller = MenuController(); +- late bool _enableFilter; ++ bool _enableFilter = false; + late List> filteredEntries; + List? _initialMenu; + int? currentHighlight; +@@ -524,6 +524,11 @@ class _DropdownMenuState extends State> { + } + _localTextEditingController = widget.controller ?? TextEditingController(); + } ++ if (oldWidget.enableFilter != widget.enableFilter) { ++ if (!widget.enableFilter) { ++ _enableFilter = false; ++ } ++ } + if (oldWidget.enableSearch != widget.enableSearch) { + if (!widget.enableSearch) { + currentHighlight = null; +@@ -663,6 +668,7 @@ class _DropdownMenuState extends State> { + ); + currentHighlight = widget.enableSearch ? i : null; + widget.onSelected?.call(entry.value); ++ _enableFilter = false; + } + : null, + requestFocusOnHover: false, +@@ -735,6 +741,8 @@ class _DropdownMenuState extends State> { + if (_enableFilter) { + filteredEntries = widget.filterCallback?.call(filteredEntries, _localTextEditingController!.text) + ?? filter(widget.dropdownMenuEntries, _localTextEditingController!); ++ } else { ++ filteredEntries = widget.dropdownMenuEntries; + } + + if (widget.enableSearch) { diff --git a/.github/workflows/flutter-build.yml b/.github/workflows/flutter-build.yml index 6fd234969777..595a604f6ac3 100644 --- a/.github/workflows/flutter-build.yml +++ b/.github/workflows/flutter-build.yml @@ -114,6 +114,15 @@ jobs: Expand-Archive -Path windows-x64-release.zip -DestinationPath windows-x64-release mv -Force windows-x64-release/*  C:/hostedtoolcache/windows/flutter/stable-${{ env.FLUTTER_VERSION }}-x64/bin/cache/artifacts/engine/windows-x64-release/ + - name: Patch flutter + shell: bash + run: | + cd $(dirname $(dirname $(which flutter))) + # https://github.com/flutter/flutter/commit/b5847d364a26d727af58ab885a6123e0e5304b2b#diff-634a338bd9ed19b66a27beba35a8acf4defffd8beff256113e6811771a0c4821R543 + PATCH_PATH="${{ github.workspace }}/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff" + PATCH_PATH=$(echo "$PATCH_PATH" | sed 's/\\/\//g') + [[ "3.24.4" == ${{env.FLUTTER_VERSION}} ]] && git apply "$PATCH_PATH" + - name: Install Rust toolchain uses: dtolnay/rust-toolchain@v1 with: @@ -486,6 +495,11 @@ jobs: channel: "stable" flutter-version: ${{ env.FLUTTER_VERSION }} + - name: Patch flutter + run: | + cd $(dirname $(dirname $(which flutter))) + [[ "3.24.4" == ${{env.FLUTTER_VERSION}} ]] && git apply ${{ github.workspace }}/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff + - name: Setup vcpkg with Github Actions binary cache uses: lukka/run-vcpkg@v11 with: @@ -687,6 +701,11 @@ jobs: channel: "stable" flutter-version: ${{ env.FLUTTER_VERSION }} + - name: Patch flutter + run: | + cd $(dirname $(dirname $(which flutter))) + [[ "3.24.4" == ${{env.FLUTTER_VERSION}} ]] && git apply ${{ github.workspace }}/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff + - name: Workaround for flutter issue shell: bash run: | @@ -922,6 +941,12 @@ jobs: with: channel: "stable" flutter-version: ${{ env.ANDROID_FLUTTER_VERSION }} + + - name: Patch flutter + run: | + cd $(dirname $(dirname $(which flutter))) + [[ "3.24.4" == ${{env.ANDROID_FLUTTER_VERSION}} ]] && git apply ${{ github.workspace }}/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff + - uses: nttld/setup-ndk@v1 id: setup-ndk with: @@ -1194,6 +1219,11 @@ jobs: channel: "stable" flutter-version: ${{ env.ANDROID_FLUTTER_VERSION }} + - name: Patch flutter + run: | + cd $(dirname $(dirname $(which flutter))) + [[ "3.24.4" == ${{env.ANDROID_FLUTTER_VERSION}} ]] && git apply ${{ github.workspace }}/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff + - name: Restore bridge files uses: actions/download-artifact@master with: @@ -1543,6 +1573,12 @@ jobs: ;; esac + if [[ "3.24.4" == ${{ env.FLUTTER_VERSION }} ]]; then + pushd $(dirname $(dirname $(which flutter))) + git apply ${{ github.workspace }}/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff + popd + fi + # build flutter pushd /workspace export CARGO_INCREMENTAL=0 @@ -2014,6 +2050,12 @@ jobs: flutter-version: ${{ env.FLUTTER_VERSION }} cache: true + - name: Patch flutter + shell: bash + run: | + cd $(dirname $(dirname $(which flutter))) + [[ "3.24.4" == ${{env.FLUTTER_VERSION}} ]] && git apply ${{ github.workspace }}/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff + # https://rustdesk.com/docs/en/dev/build/web/ - name: Build web shell: bash From f0be80c253fd964c4bab6e230d6e7a6439fd9170 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Mon, 11 Nov 2024 13:06:23 +0800 Subject: [PATCH 371/541] fix: macos, workaround app close (#9880) * fix: macos, workaround app close Signed-off-by: fufesou * Update common.dart --------- Signed-off-by: fufesou Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com> --- flutter/lib/common.dart | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 2b3bcd83cf2d..afec9fa60bcd 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -2498,9 +2498,19 @@ Future onActiveWindowChanged() async { // But the app will not close. // // No idea why we need to delay here, `terminate()` itself is also an async function. - Future.delayed(Duration.zero, () { - RdPlatformChannel.instance.terminate(); - }); + // + // A quick workaround, use `Timer.periodic` to avoid the app not closing. + // Because `await windowManager.close()` and `RdPlatformChannel.instance.terminate()` + // may not work since `Flutter 3.24.4`, see the following logs. + // A delay will allow the app to close. + // + //``` + // embedder.cc (2725): 'FlutterPlatformMessageCreateResponseHandle' returned 'kInvalidArguments'. Engine handle was invalid. + // 2024-11-11 11:41:11.546 RustDesk[90272:2567686] Failed to create a FlutterPlatformMessageResponseHandle (2) + // embedder.cc (2672): 'FlutterEngineSendPlatformMessage' returned 'kInvalidArguments'. Invalid engine handle. + // 2024-11-11 11:41:11.565 RustDesk[90272:2567686] Failed to send message to Flutter engine on channel 'flutter/lifecycle' (2). + // ``` + periodic_immediate(Duration(milliseconds: 30), RdPlatformChannel.instance.terminate); } } } From 35b4535ebc619e6def057ad8ef2fb84c6cb98610 Mon Sep 17 00:00:00 2001 From: 21pages Date: Mon, 11 Nov 2024 16:28:25 +0800 Subject: [PATCH 372/541] fix aarch64 nightly build (#9881) Signed-off-by: 21pages --- .github/workflows/flutter-build.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/flutter-build.yml b/.github/workflows/flutter-build.yml index 595a604f6ac3..87aa2b1d409d 100644 --- a/.github/workflows/flutter-build.yml +++ b/.github/workflows/flutter-build.yml @@ -1574,7 +1574,14 @@ jobs: esac if [[ "3.24.4" == ${{ env.FLUTTER_VERSION }} ]]; then - pushd $(dirname $(dirname $(which flutter))) + case ${{ matrix.job.arch }} in + aarch64) + pushd /opt/flutter-elinux/flutter + ;; + x86_64) + pushd /opt/flutter + ;; + esac git apply ${{ github.workspace }}/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff popd fi From d4aa2b7ce4bf1d23843e3398f9c4bca813964559 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Tue, 12 Nov 2024 20:12:05 +0800 Subject: [PATCH 373/541] fix: virtual display, headless, wait plug in done (#9895) Signed-off-by: fufesou --- src/virtual_display_manager.rs | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/src/virtual_display_manager.rs b/src/virtual_display_manager.rs index 41e5b3fc83cb..b2791767e884 100644 --- a/src/virtual_display_manager.rs +++ b/src/virtual_display_manager.rs @@ -529,12 +529,25 @@ pub mod amyuni_idd { } #[inline] - fn plug_monitor_(add: bool) -> Result<(), win_device::DeviceError> { + fn plug_monitor_( + add: bool, + wait_timeout: Option, + ) -> Result<(), win_device::DeviceError> { let cmd = if add { 0x10 } else { 0x00 }; let cmd = [cmd, 0x00, 0x00, 0x00]; + let now = Instant::now(); + let c1 = get_monitor_count(); unsafe { win_device::device_io_control(&INTERFACE_GUID, PLUG_MONITOR_IO_CONTROL_CDOE, &cmd, 0)?; } + if let Some(wait_timeout) = wait_timeout { + while now.elapsed() < wait_timeout { + if get_monitor_count() != c1 { + break; + } + std::thread::sleep(Duration::from_millis(30)); + } + } // No need to consider concurrency here. if add { // If the monitor is plugged in, increase the count. @@ -552,12 +565,16 @@ pub mod amyuni_idd { // `std::thread::sleep()` with a timeout is acceptable here. // Because user can wait for a while to plug in a monitor. - fn plug_in_monitor_(add: bool, is_driver_async_installed: bool) -> ResultType<()> { + fn plug_in_monitor_( + add: bool, + is_driver_async_installed: bool, + wait_timeout: Option, + ) -> ResultType<()> { let timeout = Duration::from_secs(3); let now = Instant::now(); let reg_connectivity_old = reg_display_settings::read_reg_connectivity(); loop { - match plug_monitor_(add) { + match plug_monitor_(add, wait_timeout) { Ok(_) => { break; } @@ -622,7 +639,7 @@ pub mod amyuni_idd { bail!("Failed to install driver."); } - plug_in_monitor_(true, is_async) + plug_in_monitor_(true, is_async, Some(Duration::from_millis(3_000))) } pub fn plug_in_monitor() -> ResultType<()> { @@ -636,7 +653,7 @@ pub mod amyuni_idd { bail!("There are already {VIRTUAL_DISPLAY_MAX_COUNT} monitors plugged in."); } - plug_in_monitor_(true, is_async) + plug_in_monitor_(true, is_async, None) } // `index` the display index to plug out. -1 means plug out all. @@ -700,7 +717,7 @@ pub mod amyuni_idd { } for _i in 0..to_plug_out_count { - let _ = plug_monitor_(false); + let _ = plug_monitor_(false, None); } Ok(()) } From 0aa98eac6d8b87894a9e6166cbf589215f392d94 Mon Sep 17 00:00:00 2001 From: HanaKuru Date: Tue, 12 Nov 2024 20:15:34 +0800 Subject: [PATCH 374/541] Use base64Url encoding for server configuration to ensure compatibility with the command line --config option. (#9897) --- flutter/lib/common.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index afec9fa60bcd..71e7f68d32cd 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -2603,7 +2603,7 @@ class ServerConfig { config['relay'] = relayServer.trim(); config['api'] = apiServer.trim(); config['key'] = key.trim(); - return base64Encode(Uint8List.fromList(jsonEncode(config).codeUnits)) + return base64UrlEncode(Uint8List.fromList(jsonEncode(config).codeUnits)) .split('') .reversed .join(); From ab89d84a8f8d72291efafe8bdb84c71caa99cea2 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Tue, 12 Nov 2024 22:10:56 +0800 Subject: [PATCH 375/541] refact: ci, bridge (#9899) Signed-off-by: fufesou --- .github/workflows/bridge.yml | 56 ++++------------------------- .github/workflows/flutter-build.yml | 8 ++--- 2 files changed, 10 insertions(+), 54 deletions(-) diff --git a/.github/workflows/bridge.yml b/.github/workflows/bridge.yml index f06956c18330..805ce4890afc 100644 --- a/.github/workflows/bridge.yml +++ b/.github/workflows/bridge.yml @@ -18,40 +18,15 @@ jobs: matrix: job: - { - target: x86_64-unknown-linux-gnu, - os: ubuntu-20.04, - extra-build-args: "", - } - - { - target: aarch64-apple-darwin, - os: macos-latest, - arch: aarch64, + target: x86_64-apple-darwin, + os: macos-13, + arch: x86_64, extra-build-args: "", } steps: - name: Checkout source code uses: actions/checkout@v4 - - name: Install prerequisites - if: matrix.job.os == 'ubuntu-20.04' - run: | - sudo apt-get install ca-certificates -y - sudo apt-get update -y - sudo apt-get install -y \ - clang \ - cmake \ - curl \ - gcc \ - git \ - g++ \ - libclang-10-dev \ - libgtk-3-dev \ - llvm-10-dev \ - nasm \ - ninja-build \ - pkg-config \ - wget - - name: Install Rust toolchain uses: dtolnay/rust-toolchain@v1 with: @@ -85,18 +60,10 @@ jobs: - name: Run flutter rust bridge run: | - case ${{ matrix.job.os }} in - ubuntu-20.04) - ~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart - ;; - macos-latest) - ~/.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 - ~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart --c-output ./flutter/ios/Runner/bridge_generated.h - ;; - esac + ~/.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 + cp ./flutter/macos/Runner/bridge_generated.h ./flutter/ios/Runner/bridge_generated.h - - name: Upload Artifact(ubuntu) - if: matrix.job.os == 'ubuntu-20.04' + - name: Upload Artifact uses: actions/upload-artifact@master with: name: bridge-artifact @@ -105,16 +72,5 @@ jobs: ./src/bridge_generated.io.rs ./flutter/lib/generated_bridge.dart ./flutter/lib/generated_bridge.freezed.dart - - - name: Upload Artifact(macos) - if: matrix.job.os == 'macos-latest' - uses: actions/upload-artifact@master - with: - name: bridge-artifact-macos - path: | - ./src/bridge_generated.rs - ./src/bridge_generated.io.rs - ./flutter/lib/generated_bridge.dart - ./flutter/lib/generated_bridge.freezed.dart ./flutter/macos/Runner/bridge_generated.h ./flutter/ios/Runner/bridge_generated.h diff --git a/.github/workflows/flutter-build.yml b/.github/workflows/flutter-build.yml index 87aa2b1d409d..81c415acbd29 100644 --- a/.github/workflows/flutter-build.yml +++ b/.github/workflows/flutter-build.yml @@ -408,7 +408,7 @@ jobs: - name: Restore bridge files uses: actions/download-artifact@master with: - name: bridge-artifact-macos + name: bridge-artifact path: ./ - name: Build rustdesk @@ -538,7 +538,7 @@ jobs: - name: Restore bridge files uses: actions/download-artifact@master with: - name: bridge-artifact-macos + name: bridge-artifact path: ./ - name: Build rustdesk lib @@ -593,7 +593,7 @@ jobs: - name: Restore bridge files uses: actions/download-artifact@master with: - name: bridge-artifact-macos + name: bridge-artifact path: ./ - name: Build rustdesk lib @@ -728,7 +728,7 @@ jobs: - name: Restore bridge files uses: actions/download-artifact@master with: - name: bridge-artifact-macos + name: bridge-artifact path: ./ - name: Setup vcpkg with Github Actions binary cache From 0a28d09ff84355fcfd5837255c39cb85192e8642 Mon Sep 17 00:00:00 2001 From: zyl Date: Wed, 13 Nov 2024 15:35:23 +0800 Subject: [PATCH 376/541] fix: windows, improve audio buffer (#9770) (#9893) * fix: windows, improve audio buffer (#9770) * . * fix statics does not record and avoid channel changing when drio audio when audio is stero * add some commence --------- Co-authored-by: zylthinking --- src/client.rs | 153 ++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 131 insertions(+), 22 deletions(-) diff --git a/src/client.rs b/src/client.rs index 0b5293d22ced..f7453b88dc3c 100644 --- a/src/client.rs +++ b/src/client.rs @@ -118,7 +118,7 @@ pub const SCRAP_X11_REQUIRED: &str = "x11 expected"; pub const SCRAP_X11_REF_URL: &str = "https://rustdesk.com/docs/en/manual/linux/#x11-required"; #[cfg(not(any(target_os = "android", target_os = "linux")))] -pub const AUDIO_BUFFER_MS: usize = 150; +pub const AUDIO_BUFFER_MS: usize = 3000; #[cfg(feature = "flutter")] #[cfg(not(any(target_os = "android", target_os = "ios")))] @@ -904,37 +904,124 @@ pub struct AudioHandler { } #[cfg(not(any(target_os = "android", target_os = "linux")))] -struct AudioBuffer(pub Arc>>); +struct AudioBuffer( + pub Arc>>, + usize, + [usize; 30], +); #[cfg(not(any(target_os = "android", target_os = "linux")))] impl Default for AudioBuffer { fn default() -> Self { - Self(Arc::new(std::sync::Mutex::new( - ringbuf::HeapRb::::new(48000 * 2 * AUDIO_BUFFER_MS / 1000), // 48000hz, 2 channel - ))) + Self( + Arc::new(std::sync::Mutex::new( + ringbuf::HeapRb::::new(48000 * 2 * AUDIO_BUFFER_MS / 1000), // 48000hz, 2 channel + )), + 48000 * 2, + [0; 30], + ) } } #[cfg(not(any(target_os = "android", target_os = "linux")))] impl AudioBuffer { - pub fn resize(&self, sample_rate: usize, channels: usize) { + pub fn resize(&mut self, sample_rate: usize, channels: usize) { let capacity = sample_rate * channels * AUDIO_BUFFER_MS / 1000; let old_capacity = self.0.lock().unwrap().capacity(); if capacity != old_capacity { *self.0.lock().unwrap() = ringbuf::HeapRb::::new(capacity); + self.1 = sample_rate * channels; log::info!("Audio buffer resized from {old_capacity} to {capacity}"); } } - // clear when full to avoid long time noise - #[inline] - pub fn clear_if_full(&self) { - let full = self.0.lock().unwrap().is_full(); - if full { - self.0.lock().unwrap().clear(); - log::trace!("Audio buffer cleared"); + fn try_shrink(&mut self, having: usize) { + extern crate chrono; + use chrono::prelude::*; + + let mut i = (having * 10) / self.1; + if i > 29 { + i = 29; + } + self.2[i] += 1; + + static mut tms: i64 = 0; + let dt = Local::now().timestamp_millis(); + unsafe { + if tms == 0 { + tms = dt; + return; + } else if dt < tms + 12000 { + return; + } + tms = dt; + } + + // the safer water mark to drop + let mut zero = 0; + // the water mark taking most of time + let mut max = 0; + for i in 0..30 { + if self.2[i] == 0 && zero == i { + zero += 1; + } + + if self.2[i] > self.2[max] { + self.2[max] = 0; + max = i; + } else { + self.2[i] = 0; + } + } + zero = zero * 2 / 3; + + // how many data can be dropped: + // 1. will not drop if buffered data is less than 600ms + // 2. choose based on min(zero, max) + const N: usize = 4; + self.2[max] = 0; + if max < 6 { + return; + } else if max > zero * N { + max = zero * N; + } + + let mut lock = self.0.lock().unwrap(); + let cap = lock.capacity(); + let having = lock.occupied_len(); + let skip = (cap * max / (30 * N) + 1) & (!1); + if (having > skip * 3) && (skip > 0) { + lock.skip(skip); + log::info!("skip {skip}, based {max} {zero}"); } } + + /// append pcm to audio buffer, if buffered data + /// exceeds AUDIO_BUFFER_MS, only AUDIO_BUFFER_MS + /// will be kept. + fn append_pcm2(&self, buffer: &[f32]) -> usize { + let mut lock = self.0.lock().unwrap(); + let cap = lock.capacity(); + if buffer.len() > cap { + lock.push_slice_overwrite(buffer); + return cap; + } + + let having = lock.occupied_len() + buffer.len(); + if having > cap { + lock.skip(having - cap); + } + lock.push_slice_overwrite(buffer); + lock.occupied_len() + } + + /// append pcm to audio buffer, trying to drop data + /// when data is too much (per 12 seconds) based + /// statistics. + pub fn append_pcm(&mut self, buffer: &[f32]) { + let having = self.append_pcm2(buffer); + self.try_shrink(having); + } } impl AudioHandler { @@ -993,7 +1080,9 @@ impl AudioHandler { let sample_format = config.sample_format(); log::info!("Default output format: {:?}", config); log::info!("Remote input format: {:?}", format0); - let config: StreamConfig = config.into(); + let mut config: StreamConfig = config.into(); + config.buffer_size = cpal::BufferSize::Fixed(64); + self.sample_rate = (format0.sample_rate, config.sample_rate.0); let mut build_output_stream = |config: StreamConfig| match sample_format { cpal::SampleFormat::I8 => self.build_output_stream::(&config, &device), @@ -1062,7 +1151,6 @@ impl AudioHandler { { let sample_rate0 = self.sample_rate.0; let sample_rate = self.sample_rate.1; - let audio_buffer = self.audio_buffer.0.clone(); let mut buffer = buffer[0..n].to_owned(); if sample_rate != sample_rate0 { buffer = crate::audio_resample( @@ -1081,8 +1169,7 @@ impl AudioHandler { self.device_channel, ); } - self.audio_buffer.clear_if_full(); - audio_buffer.lock().unwrap().push_slice_overwrite(&buffer); + self.audio_buffer.append_pcm(&buffer); } #[cfg(target_os = "android")] { @@ -1117,18 +1204,40 @@ impl AudioHandler { let timeout = None; let stream = device.build_output_stream( config, - move |data: &mut [T], _: &_| { + move |data: &mut [T], info: &cpal::OutputCallbackInfo| { if !*ready.lock().unwrap() { *ready.lock().unwrap() = true; } - let mut lock = audio_buffer.lock().unwrap(); + let mut n = data.len(); - if lock.occupied_len() < n { - n = lock.occupied_len(); + let mut lock = audio_buffer.lock().unwrap(); + let mut having = lock.occupied_len(); + if having < n { + let tms = info.timestamp(); + let how_long = tms + .playback + .duration_since(&tms.callback) + .unwrap_or(Duration::from_millis(0)); + + // must long enough to fight back scheuler delay + if how_long > Duration::from_millis(6) { + drop(lock); + std::thread::sleep(how_long.div_f32(1.2)); + lock = audio_buffer.lock().unwrap(); + having = lock.occupied_len(); + } + + if having < n { + n = having; + } } + let mut elems = vec![0.0f32; n]; - lock.pop_slice(&mut elems); + if n > 0 { + lock.pop_slice(&mut elems); + } drop(lock); + let mut input = elems.into_iter(); for sample in data.iter_mut() { *sample = match input.next() { From 9e4cc91a1479a55777d405976f4b025261bae6b3 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Wed, 13 Nov 2024 17:00:58 +0800 Subject: [PATCH 377/541] use linux for bridge because macos runner network problem --- .github/workflows/bridge.yml | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/.github/workflows/bridge.yml b/.github/workflows/bridge.yml index 805ce4890afc..1c0fec8d21f9 100644 --- a/.github/workflows/bridge.yml +++ b/.github/workflows/bridge.yml @@ -18,15 +18,33 @@ jobs: matrix: job: - { - target: x86_64-apple-darwin, - os: macos-13, - arch: x86_64, + target: x86_64-unknown-linux-gnu, + os: ubuntu-20.04, extra-build-args: "", } steps: - name: Checkout source code uses: actions/checkout@v4 + - name: Install prerequisites + run: | + sudo apt-get install ca-certificates -y + sudo apt-get update -y + sudo apt-get install -y \ + clang \ + cmake \ + curl \ + gcc \ + git \ + g++ \ + libclang-10-dev \ + libgtk-3-dev \ + llvm-10-dev \ + nasm \ + ninja-build \ + pkg-config \ + wget + - name: Install Rust toolchain uses: dtolnay/rust-toolchain@v1 with: From 06c7bc137fec17e4f6c1abc6fb9537d7af44b6c9 Mon Sep 17 00:00:00 2001 From: 21pages Date: Thu, 14 Nov 2024 21:01:41 +0800 Subject: [PATCH 378/541] linux android use cpal (#9914) Signed-off-by: 21pages --- Cargo.toml | 2 - build.rs | 3 +- .../com/carriez/flutter_hbb/MainActivity.kt | 18 +++ flutter/android/app/src/main/kotlin/ffi.kt | 1 + libs/scrap/src/android/ffi.rs | 58 ++++--- res/vcpkg/oboe-wrapper/CMakeLists.txt | 15 -- res/vcpkg/oboe-wrapper/oboe.cc | 118 -------------- res/vcpkg/oboe-wrapper/portfile.cmake | 8 - res/vcpkg/oboe-wrapper/vcpkg.json | 19 --- src/client.rs | 149 +----------------- vcpkg.json | 19 ++- 11 files changed, 79 insertions(+), 331 deletions(-) delete mode 100644 res/vcpkg/oboe-wrapper/CMakeLists.txt delete mode 100644 res/vcpkg/oboe-wrapper/oboe.cc delete mode 100644 res/vcpkg/oboe-wrapper/portfile.cmake delete mode 100644 res/vcpkg/oboe-wrapper/vcpkg.json diff --git a/Cargo.toml b/Cargo.toml index a7788372b958..eca0fc55c30b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -77,8 +77,6 @@ fon = "0.6" zip = "0.6" shutdown_hooks = "0.1" totp-rs = { version = "5.4", default-features = false, features = ["gen_secret", "otpauth"] } - -[target.'cfg(not(any(target_os = "android", target_os = "linux")))'.dependencies] cpal = "0.15" ringbuf = "0.3" diff --git a/build.rs b/build.rs index d332a43a22eb..7333fff7b4b1 100644 --- a/build.rs +++ b/build.rs @@ -1,7 +1,7 @@ #[cfg(windows)] fn build_windows() { let file = "src/platform/windows.cc"; - let file2 = "src/platform/windows_delete_test_cert.cc"; + 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:rerun-if-changed={}", file); @@ -72,7 +72,6 @@ fn install_android_deps() { ); println!("cargo:rustc-link-lib=ndk_compat"); println!("cargo:rustc-link-lib=oboe"); - println!("cargo:rustc-link-lib=oboe_wrapper"); println!("cargo:rustc-link-lib=c++"); println!("cargo:rustc-link-lib=OpenSLES"); } diff --git a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainActivity.kt b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainActivity.kt index 10c3d7c2d78f..6b197a39f8ae 100644 --- a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainActivity.kt +++ b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainActivity.kt @@ -30,6 +30,10 @@ import com.hjq.permissions.XXPermissions import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.engine.FlutterEngine import io.flutter.plugin.common.MethodChannel +import io.flutter.embedding.engine.plugins.FlutterPlugin +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel.MethodCallHandler +import io.flutter.plugin.common.MethodChannel.Result import kotlin.concurrent.thread @@ -57,6 +61,7 @@ class MainActivity : FlutterActivity() { channelTag ) initFlutterChannel(flutterMethodChannel!!) + flutterEngine.plugins.add(ContextPlugin()) thread { setCodecInfo() } } @@ -389,3 +394,16 @@ class MainActivity : FlutterActivity() { stopService(Intent(this, FloatingWindowService::class.java)) } } + +// https://cjycode.com/flutter_rust_bridge/guides/how-to/ndk-init +class ContextPlugin : FlutterPlugin, MethodCallHandler { + override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { + FFI.initContext(flutterPluginBinding.applicationContext) + } + override fun onMethodCall(call: MethodCall, result: Result) { + result.notImplemented() + } + + override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { + } +} \ No newline at end of file diff --git a/flutter/android/app/src/main/kotlin/ffi.kt b/flutter/android/app/src/main/kotlin/ffi.kt index a7573bbf9eed..43368c1e6d4b 100644 --- a/flutter/android/app/src/main/kotlin/ffi.kt +++ b/flutter/android/app/src/main/kotlin/ffi.kt @@ -11,6 +11,7 @@ object FFI { } external fun init(ctx: Context) + external fun initContext(ctx: Context) external fun startServer(app_dir: String, custom_client_config: String) external fun startService() external fun onVideoFrameUpdate(buf: ByteBuffer) diff --git a/libs/scrap/src/android/ffi.rs b/libs/scrap/src/android/ffi.rs index f5208a673b27..4608597ce6d7 100644 --- a/libs/scrap/src/android/ffi.rs +++ b/libs/scrap/src/android/ffi.rs @@ -12,6 +12,7 @@ use jni::errors::{Error as JniError, Result as JniResult}; use lazy_static::lazy_static; use serde::Deserialize; use std::ops::Not; +use std::os::raw::c_void; use std::sync::atomic::{AtomicPtr, Ordering::SeqCst}; use std::sync::{Mutex, RwLock}; use std::time::{Duration, Instant}; @@ -155,10 +156,24 @@ pub extern "system" fn Java_ffi_FFI_setFrameRawEnable( pub extern "system" fn Java_ffi_FFI_init(env: JNIEnv, _class: JClass, ctx: JObject) { log::debug!("MainService init from java"); if let Ok(jvm) = env.get_java_vm() { + let java_vm = jvm.get_java_vm_pointer() as *mut c_void; *JVM.write().unwrap() = Some(jvm); if let Ok(context) = env.new_global_ref(ctx) { + let context_jobject = context.as_obj().as_raw() as *mut c_void; *MAIN_SERVICE_CTX.write().unwrap() = Some(context); - init_ndk_context().ok(); + init_ndk_context(java_vm, context_jobject); + } + } +} + +#[no_mangle] +pub extern "system" fn Java_ffi_FFI_initContext(env: JNIEnv, _class: JClass, ctx: JObject) { + log::debug!("MainActivity initContext from java"); + if let Ok(jvm) = env.get_java_vm() { + if let Ok(context) = env.new_global_ref(ctx) { + let java_vm = jvm.get_java_vm_pointer() as *mut c_void; + let context_jobject = context.as_obj().as_raw() as *mut c_void; + init_ndk_context(java_vm, context_jobject); } } } @@ -332,7 +347,14 @@ pub fn call_main_service_set_by_name( } } -fn init_ndk_context() -> JniResult<()> { +// Difference between MainService, MainActivity, JNI_OnLoad: +// jvm is the same, ctx is differen and ctx of JNI_OnLoad is null. +// cpal: all three works +// Service(GetByName, ...): only ctx from MainService works, so use 2 init context functions +// On app start: JNI_OnLoad or MainActivity init context +// On service start first time: MainService replace the context + +fn init_ndk_context(java_vm: *mut c_void, context_jobject: *mut c_void) { let mut lock = NDK_CONTEXT_INITED.lock().unwrap(); if *lock { unsafe { @@ -340,22 +362,20 @@ fn init_ndk_context() -> JniResult<()> { } *lock = false; } - if let (Some(jvm), Some(ctx)) = ( - JVM.read().unwrap().as_ref(), - MAIN_SERVICE_CTX.read().unwrap().as_ref(), - ) { - unsafe { - ndk_context::initialize_android_context( - jvm.get_java_vm_pointer() as _, - ctx.as_obj().as_raw() as _, - ); - #[cfg(feature = "hwcodec")] - hwcodec::android::ffmpeg_set_java_vm( - jvm.get_java_vm_pointer() as _, - ); - } - *lock = true; - return Ok(()); + unsafe { + ndk_context::initialize_android_context(java_vm, context_jobject); + #[cfg(feature = "hwcodec")] + hwcodec::android::ffmpeg_set_java_vm(java_vm); } - Err(JniError::ThrowFailed(-1)) + *lock = true; } + +// // https://cjycode.com/flutter_rust_bridge/guides/how-to/ndk-init +// #[no_mangle] +// pub extern "C" fn JNI_OnLoad(vm: jni::JavaVM, res: *mut std::os::raw::c_void) -> jni::sys::jint { +// if let Ok(env) = vm.get_env() { +// let vm = vm.get_java_vm_pointer() as *mut std::os::raw::c_void; +// init_ndk_context(vm, res); +// } +// jni::JNIVersion::V6.into() +// } diff --git a/res/vcpkg/oboe-wrapper/CMakeLists.txt b/res/vcpkg/oboe-wrapper/CMakeLists.txt deleted file mode 100644 index 9d50a989498c..000000000000 --- a/res/vcpkg/oboe-wrapper/CMakeLists.txt +++ /dev/null @@ -1,15 +0,0 @@ -cmake_minimum_required(VERSION 3.20) -project(oboe_wrapper CXX) - -include(GNUInstallDirs) - -add_library(oboe_wrapper STATIC - oboe.cc -) - -target_include_directories(oboe_wrapper PRIVATE "${CURRENT_INSTALLED_DIR}/include") - -install(TARGETS oboe_wrapper - ARCHIVE DESTINATION "${CMAKE_INSTALL_LIBDIR}" - LIBRARY DESTINATION "${CMAKE_INSTALL_LIBDIR}" - RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}") diff --git a/res/vcpkg/oboe-wrapper/oboe.cc b/res/vcpkg/oboe-wrapper/oboe.cc deleted file mode 100644 index a3c8238a7027..000000000000 --- a/res/vcpkg/oboe-wrapper/oboe.cc +++ /dev/null @@ -1,118 +0,0 @@ -#include -#include -#include -#include - -// I got link problem with std::mutex, so use pthread instead -class CThreadLock -{ -public: - CThreadLock(); - virtual ~CThreadLock(); - - void Lock(); - void Unlock(); - -private: - pthread_mutex_t mutexlock; -}; - -CThreadLock::CThreadLock() -{ - // init lock here - pthread_mutex_init(&mutexlock, 0); -} - -CThreadLock::~CThreadLock() -{ - // deinit lock here - pthread_mutex_destroy(&mutexlock); -} -void CThreadLock::Lock() -{ - // lock - pthread_mutex_lock(&mutexlock); -} -void CThreadLock::Unlock() -{ - // unlock - pthread_mutex_unlock(&mutexlock); -} - -class Player : public oboe::AudioStreamDataCallback -{ -public: - Player(int channels, int sample_rate) - { - this->channels = channels; - oboe::AudioStreamBuilder builder; - // The builder set methods can be chained for convenience. - builder.setSharingMode(oboe::SharingMode::Exclusive) - ->setPerformanceMode(oboe::PerformanceMode::LowLatency) - ->setChannelCount(channels) - ->setSampleRate(sample_rate) - ->setFormat(oboe::AudioFormat::Float) - ->setDataCallback(this) - ->openManagedStream(outStream); - // Typically, start the stream after querying some stream information, as well as some input from the user - outStream->requestStart(); - } - - ~Player() { - outStream->requestStop(); - } - - oboe::DataCallbackResult onAudioReady(oboe::AudioStream *oboeStream, void *audioData, int32_t numFrames) override - { - float *floatData = (float *)audioData; - int i = 0; - mtx.Lock(); - auto n = channels * numFrames; - for (; i < n && i < (int)buffer.size(); ++i, ++floatData) - { - *floatData = buffer.front(); - buffer.pop_front(); - } - mtx.Unlock(); - for (; i < n; ++i, ++floatData) - { - *floatData = 0; - } - return oboe::DataCallbackResult::Continue; - } - - void push(const float *v, int n) - { - mtx.Lock(); - for (auto i = 0; i < n; ++i, ++v) - buffer.push_back(*v); - // in case memory overuse - if (buffer.size() > 48 * 1024 * 120) - buffer.clear(); - mtx.Unlock(); - } - -private: - oboe::ManagedStream outStream; - int channels; - std::deque buffer; - CThreadLock mtx; -}; - -extern "C" -{ - void *create_oboe_player(int channels, int sample_rate) - { - return new Player(channels, sample_rate); - } - - void push_oboe_data(void *player, const float* v, int n) - { - static_cast(player)->push(v, n); - } - - void destroy_oboe_player(void *player) - { - delete static_cast(player); - } -} \ No newline at end of file diff --git a/res/vcpkg/oboe-wrapper/portfile.cmake b/res/vcpkg/oboe-wrapper/portfile.cmake deleted file mode 100644 index c83f5bcb1d3f..000000000000 --- a/res/vcpkg/oboe-wrapper/portfile.cmake +++ /dev/null @@ -1,8 +0,0 @@ -vcpkg_configure_cmake( - SOURCE_PATH "${CMAKE_CURRENT_LIST_DIR}" - OPTIONS - -DCURRENT_INSTALLED_DIR=${CURRENT_INSTALLED_DIR} - PREFER_NINJA -) - -vcpkg_cmake_install() diff --git a/res/vcpkg/oboe-wrapper/vcpkg.json b/res/vcpkg/oboe-wrapper/vcpkg.json deleted file mode 100644 index be497e1bb261..000000000000 --- a/res/vcpkg/oboe-wrapper/vcpkg.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "name": "oboe-wrapper", - "version": "0", - "description": "None", - "dependencies": [ - { - "name": "vcpkg-cmake", - "host": true - }, - { - "name": "vcpkg-cmake-config", - "host": true - }, - { - "name": "oboe", - "host": false - } - ] -} diff --git a/src/client.rs b/src/client.rs index f7453b88dc3c..2abc4d0ff017 100644 --- a/src/client.rs +++ b/src/client.rs @@ -2,14 +2,12 @@ use async_trait::async_trait; use bytes::Bytes; #[cfg(not(any(target_os = "android", target_os = "ios")))] use clipboard_master::{CallbackResult, ClipboardHandler}; -#[cfg(not(any(target_os = "android", target_os = "linux")))] use cpal::{ traits::{DeviceTrait, HostTrait, StreamTrait}, Device, Host, StreamConfig, }; use crossbeam_queue::ArrayQueue; use magnum_opus::{Channels::*, Decoder as AudioDecoder}; -#[cfg(not(any(target_os = "android", target_os = "linux")))] use ringbuf::{ring_buffer::RbBase, Rb}; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; @@ -117,7 +115,6 @@ pub const SCRAP_OTHER_VERSION_OR_X11_REQUIRED: &str = pub const SCRAP_X11_REQUIRED: &str = "x11 expected"; pub const SCRAP_X11_REF_URL: &str = "https://rustdesk.com/docs/en/manual/linux/#x11-required"; -#[cfg(not(any(target_os = "android", target_os = "linux")))] pub const AUDIO_BUFFER_MS: usize = 3000; #[cfg(feature = "flutter")] @@ -140,7 +137,6 @@ struct TextClipboardState { running: bool, } -#[cfg(not(any(target_os = "android", target_os = "linux")))] lazy_static::lazy_static! { static ref AUDIO_HOST: Host = cpal::default_host(); } @@ -163,66 +159,6 @@ pub fn get_key_state(key: enigo::Key) -> bool { ENIGO.lock().unwrap().get_key_state(key) } -cfg_if::cfg_if! { - if #[cfg(target_os = "android")] { - -use hbb_common::libc::{c_float, c_int}; -type Oboe = *mut c_void; -extern "C" { - fn create_oboe_player(channels: c_int, sample_rate: c_int) -> Oboe; - fn push_oboe_data(oboe: Oboe, d: *const c_float, n: c_int); - fn destroy_oboe_player(oboe: Oboe); -} - -struct OboePlayer { - raw: Oboe, -} - -impl Default for OboePlayer { - fn default() -> Self { - Self { - raw: std::ptr::null_mut(), - } - } -} - -impl OboePlayer { - fn new(channels: i32, sample_rate: i32) -> Self { - unsafe { - Self { - raw: create_oboe_player(channels, sample_rate), - } - } - } - - #[cfg(not(any(target_os = "android", target_os = "ios")))] - fn is_null(&self) -> bool { - self.raw.is_null() - } - - fn push(&mut self, d: &[f32]) { - if self.raw.is_null() { - return; - } - unsafe { - push_oboe_data(self.raw, d.as_ptr(), d.len() as _); - } - } -} - -impl Drop for OboePlayer { - fn drop(&mut self) { - unsafe { - if !self.raw.is_null() { - destroy_oboe_player(self.raw); - } - } - } -} - -} -} - impl Client { /// Start a new connection. pub async fn start( @@ -887,30 +823,20 @@ impl ClipboardHandler for ClientClipboardHandler { #[derive(Default)] pub struct AudioHandler { audio_decoder: Option<(AudioDecoder, Vec)>, - #[cfg(target_os = "android")] - oboe: Option, - #[cfg(target_os = "linux")] - simple: Option, - #[cfg(not(any(target_os = "android", target_os = "linux")))] audio_buffer: AudioBuffer, sample_rate: (u32, u32), - #[cfg(not(any(target_os = "android", target_os = "linux")))] audio_stream: Option>, channels: u16, - #[cfg(not(any(target_os = "android", target_os = "linux")))] device_channel: u16, - #[cfg(not(any(target_os = "android", target_os = "linux")))] ready: Arc>, } -#[cfg(not(any(target_os = "android", target_os = "linux")))] struct AudioBuffer( pub Arc>>, usize, [usize; 30], ); -#[cfg(not(any(target_os = "android", target_os = "linux")))] impl Default for AudioBuffer { fn default() -> Self { Self( @@ -923,7 +849,6 @@ impl Default for AudioBuffer { } } -#[cfg(not(any(target_os = "android", target_os = "linux")))] impl AudioBuffer { pub fn resize(&mut self, sample_rate: usize, channels: usize) { let capacity = sample_rate * channels * AUDIO_BUFFER_MS / 1000; @@ -1026,48 +951,6 @@ impl AudioBuffer { impl AudioHandler { /// Start the audio playback. - #[cfg(target_os = "linux")] - fn start_audio(&mut self, format0: AudioFormat) -> ResultType<()> { - use psimple::Simple; - use pulse::sample::{Format, Spec}; - use pulse::stream::Direction; - - let spec = Spec { - format: Format::F32le, - channels: format0.channels as _, - rate: format0.sample_rate as _, - }; - if !spec.is_valid() { - bail!("Invalid audio format"); - } - - self.simple = Some(Simple::new( - None, // Use the default server - &crate::get_app_name(), // Our application’s name - Direction::Playback, // We want a playback stream - None, // Use the default device - "playback", // Description of our stream - &spec, // Our sample format - None, // Use default channel map - None, // Use default buffering attributes - )?); - self.sample_rate = (format0.sample_rate, format0.sample_rate); - Ok(()) - } - - /// Start the audio playback. - #[cfg(target_os = "android")] - fn start_audio(&mut self, format0: AudioFormat) -> ResultType<()> { - self.oboe = Some(OboePlayer::new( - format0.channels as _, - format0.sample_rate as _, - )); - self.sample_rate = (format0.sample_rate, format0.sample_rate); - Ok(()) - } - - /// Start the audio playback. - #[cfg(not(any(target_os = "android", target_os = "linux")))] fn start_audio(&mut self, format0: AudioFormat) -> ResultType<()> { let device = AUDIO_HOST .default_output_device() @@ -1130,24 +1013,13 @@ impl AudioHandler { /// Handle audio frame and play it. #[inline] pub fn handle_frame(&mut self, frame: AudioFrame) { - #[cfg(not(any(target_os = "android", target_os = "linux")))] if self.audio_stream.is_none() || !self.ready.lock().unwrap().clone() { return; } - #[cfg(target_os = "linux")] - if self.simple.is_none() { - log::debug!("PulseAudio simple binding does not exists"); - return; - } - #[cfg(target_os = "android")] - if self.oboe.is_none() { - return; - } self.audio_decoder.as_mut().map(|(d, buffer)| { if let Ok(n) = d.decode_float(&frame.data, buffer, false) { let channels = self.channels; let n = n * (channels as usize); - #[cfg(not(any(target_os = "android", target_os = "linux")))] { let sample_rate0 = self.sample_rate.0; let sample_rate = self.sample_rate.1; @@ -1171,22 +1043,11 @@ impl AudioHandler { } self.audio_buffer.append_pcm(&buffer); } - #[cfg(target_os = "android")] - { - self.oboe.as_mut().map(|x| x.push(&buffer[0..n])); - } - #[cfg(target_os = "linux")] - { - let data_u8 = - unsafe { std::slice::from_raw_parts::(buffer.as_ptr() as _, n * 4) }; - self.simple.as_mut().map(|x| x.write(data_u8)); - } } }); } /// Build audio output stream for current device. - #[cfg(not(any(target_os = "android", target_os = "linux")))] fn build_output_stream>( &mut self, config: &StreamConfig, @@ -1212,6 +1073,8 @@ impl AudioHandler { let mut n = data.len(); let mut lock = audio_buffer.lock().unwrap(); let mut having = lock.occupied_len(); + // android two timestamps, one from zero, another not + #[cfg(not(target_os = "android"))] if having < n { let tms = info.timestamp(); let how_long = tms @@ -1220,7 +1083,8 @@ impl AudioHandler { .unwrap_or(Duration::from_millis(0)); // must long enough to fight back scheuler delay - if how_long > Duration::from_millis(6) { + if how_long > Duration::from_millis(6) && how_long < Duration::from_millis(3000) + { drop(lock); std::thread::sleep(how_long.div_f32(1.2)); lock = audio_buffer.lock().unwrap(); @@ -1231,7 +1095,10 @@ impl AudioHandler { n = having; } } - + #[cfg(target_os = "android")] + if having < n { + n = having; + } let mut elems = vec![0.0f32; n]; if n > 0 { lock.pop_slice(&mut elems); diff --git a/vcpkg.json b/vcpkg.json index 81484772aea1..75cee85b1502 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -24,10 +24,6 @@ "name": "oboe", "platform": "android" }, - { - "name": "oboe-wrapper", - "platform": "android" - }, { "name": "opus", "host": true @@ -87,8 +83,17 @@ ] }, "overrides": [ - { "name": "ffnvcodec", "version": "12.1.14.0" }, - { "name": "amd-amf", "version": "1.4.29" }, - { "name": "mfx-dispatch", "version": "1.35.1" } + { + "name": "ffnvcodec", + "version": "12.1.14.0" + }, + { + "name": "amd-amf", + "version": "1.4.29" + }, + { + "name": "mfx-dispatch", + "version": "1.35.1" + } ] } \ No newline at end of file From ddc172bdfa299534e1a8653feee70fa4ee56a83f Mon Sep 17 00:00:00 2001 From: RustDesk <71636191+rustdesk@users.noreply.github.com> Date: Sat, 16 Nov 2024 16:26:26 +0800 Subject: [PATCH 379/541] Update bug_report.yaml (#9934) --- .github/ISSUE_TEMPLATE/bug_report.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index 0615604ed529..cbfcd888a75c 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -26,7 +26,7 @@ body: - type: input id: os attributes: - label: Operating system(s) on local side and remote side + label: Operating system(s) on local (controlling) side and remote (controlled) side description: What operating system(s) do you see this bug on? local (controlling) side -> remote (controlled) side. placeholder: | Windows 10 -> osx @@ -35,7 +35,7 @@ body: - type: input id: version attributes: - label: RustDesk Version(s) on local side and remote side + label: RustDesk Version(s) on local (controlling) side and remote (controlled) side description: What RustDesk version(s) do you see this bug on? local side -> remote side. placeholder: | 1.1.9 -> 1.1.8 From 5d2bb9c995b8e050a9ef6269f9e329038ff3930c Mon Sep 17 00:00:00 2001 From: RustDesk <71636191+rustdesk@users.noreply.github.com> Date: Sat, 16 Nov 2024 16:27:51 +0800 Subject: [PATCH 380/541] Update bug_report.yaml (#9935) --- .github/ISSUE_TEMPLATE/bug_report.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index cbfcd888a75c..dbb8ed8a92e9 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -36,7 +36,7 @@ body: id: version attributes: label: RustDesk Version(s) on local (controlling) side and remote (controlled) side - description: What RustDesk version(s) do you see this bug on? local side -> remote side. + description: What RustDesk version(s) do you see this bug on? local (controlling) side -> remote (controlled) side. placeholder: | 1.1.9 -> 1.1.8 validations: From 304e0e465db6c4bf3434fa80d7367f18a1e16160 Mon Sep 17 00:00:00 2001 From: 21pages Date: Sat, 16 Nov 2024 22:31:28 +0800 Subject: [PATCH 381/541] if av1 is not slow in test, av1 takes precedence over vp9 (#9938) Signed-off-by: 21pages --- libs/hbb_common/src/config.rs | 1 + libs/scrap/src/common/codec.rs | 94 ++++++++++++++++++++++++++++++++-- src/flutter_ffi.rs | 1 + src/server.rs | 2 +- 4 files changed, 93 insertions(+), 5 deletions(-) diff --git a/libs/hbb_common/src/config.rs b/libs/hbb_common/src/config.rs index 5807dafa5b4a..dd4abaf9d9b9 100644 --- a/libs/hbb_common/src/config.rs +++ b/libs/hbb_common/src/config.rs @@ -2238,6 +2238,7 @@ pub mod keys { pub const OPTION_ENABLE_ANDROID_SOFTWARE_ENCODING_HALF_SCALE: &str = "enable-android-software-encoding-half-scale"; pub const OPTION_ENABLE_TRUSTED_DEVICES: &str = "enable-trusted-devices"; + pub const OPTION_AV1_TEST: &str = "av1-test"; // buildin options pub const OPTION_DISPLAY_NAME: &str = "display-name"; diff --git a/libs/scrap/src/common/codec.rs b/libs/scrap/src/common/codec.rs index dad924c862cd..0e4b65da67ae 100644 --- a/libs/scrap/src/common/codec.rs +++ b/libs/scrap/src/common/codec.rs @@ -1,6 +1,5 @@ use std::{ collections::HashMap, - ffi::c_void, ops::{Deref, DerefMut}, sync::{Arc, Mutex}, }; @@ -264,15 +263,20 @@ impl Encoder { .unwrap_or((PreferCodec::Auto.into(), 0)); let preference = most_frequent.enum_value_or(PreferCodec::Auto); - // auto: h265 > h264 > vp9/vp8 - let mut auto_codec = CodecFormat::VP9; + // auto: h265 > h264 > av1/vp9/vp8 + let av1_test = Config::get_option(hbb_common::config::keys::OPTION_AV1_TEST) != "N"; + let mut auto_codec = if av1_useable && av1_test { + CodecFormat::AV1 + } else { + CodecFormat::VP9 + }; if h264_useable { auto_codec = CodecFormat::H264; } if h265_useable { auto_codec = CodecFormat::H265; } - if auto_codec == CodecFormat::VP9 { + if auto_codec == CodecFormat::VP9 || auto_codec == CodecFormat::AV1 { let mut system = System::new(); system.refresh_memory(); if vp8_useable && system.total_memory() <= 4 * 1024 * 1024 * 1024 { @@ -982,3 +986,85 @@ fn disable_av1() -> bool { // disable it for all 32 bit platforms std::mem::size_of::() == 4 } + +#[cfg(not(target_os = "ios"))] +pub fn test_av1() { + use hbb_common::config::keys::OPTION_AV1_TEST; + use std::sync::Once; + if disable_av1() || !Config::get_option(OPTION_AV1_TEST).is_empty() { + log::info!("skip test av1"); + return; + } + + static ONCE: Once = Once::new(); + ONCE.call_once(|| { + let f = || { + let (width, height, quality, keyframe_interval, i444) = + (1920, 1080, Quality::Balanced, None, false); + let Ok(mut av1) = AomEncoder::new( + EncoderCfg::AOM(AomEncoderConfig { + width, + height, + quality, + keyframe_interval, + }), + i444, + ) else { + return false; + }; + let Ok(mut vp9) = VpxEncoder::new( + EncoderCfg::VPX(VpxEncoderConfig { + codec: VpxVideoCodecId::VP9, + width, + height, + quality, + keyframe_interval, + }), + i444, + ) else { + return true; + }; + let frame_count = 10; + let mut yuv = vec![0u8; (width * height * 3 / 2) as usize]; + let start = Instant::now(); + for i in 0..frame_count { + for w in 0..width { + yuv[w as usize] = i as u8; + } + if av1.encode(0, &yuv, super::STRIDE_ALIGN).is_err() { + log::debug!("av1 encode failed"); + if i == 0 { + return false; + } + } + } + let av1_time = start.elapsed(); + log::info!("av1 time: {:?}", av1_time); + if av1_time < std::time::Duration::from_millis(250) { + return true; + } + let start = Instant::now(); + for i in 0..frame_count { + for w in 0..width { + yuv[w as usize] = i as u8; + } + if vp9.encode(0, &yuv, super::STRIDE_ALIGN).is_err() { + log::debug!("vp9 encode failed"); + if i == 0 { + return true; + } + } + } + let vp9_time = start.elapsed(); + log::info!("vp9 time: {:?}", vp9_time); + av1_time * 2 / 3 < vp9_time + }; + std::thread::spawn(move || { + let v = f(); + Config::set_option( + OPTION_AV1_TEST.to_string(), + if v { "Y" } else { "N" }.to_string(), + ); + }); + }); +} diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 7a0c5e87449d..3943f30f15ae 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -2332,6 +2332,7 @@ pub mod server_side { } } std::thread::spawn(move || start_server(true)); + scrap::codec::test_av1(); } #[no_mangle] diff --git a/src/server.rs b/src/server.rs index eca20b829bba..02c4df36c179 100644 --- a/src/server.rs +++ b/src/server.rs @@ -500,8 +500,8 @@ pub async fn start_server(is_server: bool, no_server: bool) { #[cfg(target_os = "windows")] crate::platform::try_kill_broker(); #[cfg(feature = "hwcodec")] - #[cfg(not(any(target_os = "android", target_os = "ios")))] scrap::hwcodec::start_check_process(); + scrap::codec::test_av1(); crate::RendezvousMediator::start_all().await; } else { match crate::ipc::connect(1000, "").await { From 9ee77a9b9283246f66002b7330c3271ba32e1dde Mon Sep 17 00:00:00 2001 From: 21pages Date: Sun, 17 Nov 2024 09:21:34 +0800 Subject: [PATCH 382/541] fix last commit (#9939) Signed-off-by: 21pages --- src/flutter_ffi.rs | 1 - src/rendezvous_mediator.rs | 1 + src/server.rs | 1 - 3 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 3943f30f15ae..7a0c5e87449d 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -2332,7 +2332,6 @@ pub mod server_side { } } std::thread::spawn(move || start_server(true)); - scrap::codec::test_av1(); } #[no_mangle] diff --git a/src/rendezvous_mediator.rs b/src/rendezvous_mediator.rs index f5d81eaffa63..69fc886cac96 100644 --- a/src/rendezvous_mediator.rs +++ b/src/rendezvous_mediator.rs @@ -90,6 +90,7 @@ impl RendezvousMediator { if crate::is_server() { crate::platform::linux_desktop_manager::start_xdesktop(); } + scrap::codec::test_av1(); loop { let conn_start_time = Instant::now(); *SOLVING_PK_MISMATCH.lock().await = "".to_owned(); diff --git a/src/server.rs b/src/server.rs index 02c4df36c179..46c30b8bcf4b 100644 --- a/src/server.rs +++ b/src/server.rs @@ -501,7 +501,6 @@ pub async fn start_server(is_server: bool, no_server: bool) { crate::platform::try_kill_broker(); #[cfg(feature = "hwcodec")] scrap::hwcodec::start_check_process(); - scrap::codec::test_av1(); crate::RendezvousMediator::start_all().await; } else { match crate::ipc::connect(1000, "").await { From 9125a68f81f8b219a6b2aeb690a3aca7763ddf65 Mon Sep 17 00:00:00 2001 From: 21pages Date: Sun, 17 Nov 2024 11:35:53 +0800 Subject: [PATCH 383/541] remove flutter install cache flag (#9944) Signed-off-by: 21pages --- .github/workflows/flutter-build.yml | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/.github/workflows/flutter-build.yml b/.github/workflows/flutter-build.yml index 81c415acbd29..fd169e1dd9b2 100644 --- a/.github/workflows/flutter-build.yml +++ b/.github/workflows/flutter-build.yml @@ -103,7 +103,6 @@ jobs: with: channel: "stable" flutter-version: ${{ env.FLUTTER_VERSION }} - cache: true # https://github.com/flutter/flutter/issues/155685 - name: Replace engine with rustdesk custom flutter engine @@ -117,11 +116,9 @@ jobs: - name: Patch flutter shell: bash run: | + cp .github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff $(dirname $(dirname $(which flutter))) cd $(dirname $(dirname $(which flutter))) - # https://github.com/flutter/flutter/commit/b5847d364a26d727af58ab885a6123e0e5304b2b#diff-634a338bd9ed19b66a27beba35a8acf4defffd8beff256113e6811771a0c4821R543 - PATCH_PATH="${{ github.workspace }}/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff" - PATCH_PATH=$(echo "$PATCH_PATH" | sed 's/\\/\//g') - [[ "3.24.4" == ${{env.FLUTTER_VERSION}} ]] && git apply "$PATCH_PATH" + [[ "3.24.4" == ${{env.FLUTTER_VERSION}} ]] && git apply flutter_3.24.4_dropdown_menu_enableFilter.diff - name: Install Rust toolchain uses: dtolnay/rust-toolchain@v1 @@ -2055,7 +2052,6 @@ jobs: with: channel: "stable" flutter-version: ${{ env.FLUTTER_VERSION }} - cache: true - name: Patch flutter shell: bash From a07392e6b82eca20065bcadd47af2bfb097dd936 Mon Sep 17 00:00:00 2001 From: XLion Date: Sun, 17 Nov 2024 21:07:00 +0800 Subject: [PATCH 384/541] Add URL and Vendor to RPM spec (#9947) --- res/rpm-flutter-suse.spec | 2 ++ res/rpm-flutter.spec | 2 ++ res/rpm.spec | 2 ++ 3 files changed, 6 insertions(+) diff --git a/res/rpm-flutter-suse.spec b/res/rpm-flutter-suse.spec index 7f52a8f57f87..06653a8ce7f0 100644 --- a/res/rpm-flutter-suse.spec +++ b/res/rpm-flutter-suse.spec @@ -3,6 +3,8 @@ Version: 1.3.3 Release: 0 Summary: RPM package License: GPL-3.0 +URL: https://rustdesk.com +Vendor: rustdesk Requires: gtk3 libxcb1 xdotool libXfixes3 alsa-utils libXtst6 libvdpau1 libva2 pam gstreamer-plugins-base gstreamer-plugin-pipewire Recommends: libayatana-appindicator3-1 Provides: libdesktop_drop_plugin.so()(64bit), libdesktop_multi_window_plugin.so()(64bit), libfile_selector_linux_plugin.so()(64bit), libflutter_custom_cursor_plugin.so()(64bit), libflutter_linux_gtk.so()(64bit), libscreen_retriever_plugin.so()(64bit), libtray_manager_plugin.so()(64bit), liburl_launcher_linux_plugin.so()(64bit), libwindow_manager_plugin.so()(64bit), libwindow_size_plugin.so()(64bit), libtexture_rgba_renderer_plugin.so()(64bit) diff --git a/res/rpm-flutter.spec b/res/rpm-flutter.spec index fcb15a1a14ab..74115f8877c1 100644 --- a/res/rpm-flutter.spec +++ b/res/rpm-flutter.spec @@ -3,6 +3,8 @@ Version: 1.3.3 Release: 0 Summary: RPM package License: GPL-3.0 +URL: https://rustdesk.com +Vendor: rustdesk Requires: gtk3 libxcb libxdo libXfixes alsa-lib libvdpau libva pam gstreamer1-plugins-base Recommends: libayatana-appindicator-gtk3 Provides: libdesktop_drop_plugin.so()(64bit), libdesktop_multi_window_plugin.so()(64bit), libfile_selector_linux_plugin.so()(64bit), libflutter_custom_cursor_plugin.so()(64bit), libflutter_linux_gtk.so()(64bit), libscreen_retriever_plugin.so()(64bit), libtray_manager_plugin.so()(64bit), liburl_launcher_linux_plugin.so()(64bit), libwindow_manager_plugin.so()(64bit), libwindow_size_plugin.so()(64bit), libtexture_rgba_renderer_plugin.so()(64bit) diff --git a/res/rpm.spec b/res/rpm.spec index bdd6afa3f4f6..3dfc496c2ec8 100644 --- a/res/rpm.spec +++ b/res/rpm.spec @@ -3,6 +3,8 @@ Version: 1.3.3 Release: 0 Summary: RPM package License: GPL-3.0 +URL: https://rustdesk.com +Vendor: rustdesk Requires: gtk3 libxcb libxdo libXfixes alsa-lib libvdpau1 libva2 pam gstreamer1-plugins-base Recommends: libayatana-appindicator-gtk3 From 0707e791e852f096eb31730aa80cf16650151dbd Mon Sep 17 00:00:00 2001 From: 21pages Date: Mon, 18 Nov 2024 15:05:23 +0800 Subject: [PATCH 385/541] opt av1 test data (#9954) Signed-off-by: 21pages --- libs/scrap/src/common/codec.rs | 119 ++++++++++++++++++++++----------- 1 file changed, 79 insertions(+), 40 deletions(-) diff --git a/libs/scrap/src/common/codec.rs b/libs/scrap/src/common/codec.rs index 0e4b65da67ae..648de19b069d 100644 --- a/libs/scrap/src/common/codec.rs +++ b/libs/scrap/src/common/codec.rs @@ -2,6 +2,7 @@ use std::{ collections::HashMap, ops::{Deref, DerefMut}, sync::{Arc, Mutex}, + time::Instant, }; #[cfg(feature = "hwcodec")] @@ -27,7 +28,6 @@ use hbb_common::{ SupportedDecoding, SupportedEncoding, VideoFrame, }, sysinfo::System, - tokio::time::Instant, ResultType, }; @@ -990,7 +990,9 @@ fn disable_av1() -> bool { #[cfg(not(target_os = "ios"))] pub fn test_av1() { use hbb_common::config::keys::OPTION_AV1_TEST; - use std::sync::Once; + use hbb_common::rand::Rng; + use std::{sync::Once, time::Duration}; + if disable_av1() || !Config::get_option(OPTION_AV1_TEST).is_empty() { log::info!("skip test av1"); return; @@ -1001,6 +1003,56 @@ pub fn test_av1() { let f = || { let (width, height, quality, keyframe_interval, i444) = (1920, 1080, Quality::Balanced, None, false); + let frame_count = 10; + let block_size = 300; + let move_step = 50; + let generate_fake_data = + |frame_index: u32, dst_fmt: EncodeYuvFormat| -> ResultType> { + let mut rng = hbb_common::rand::thread_rng(); + let mut bgra = vec![0u8; (width * height * 4) as usize]; + let gradient = frame_index as f32 / frame_count as f32; + // floating block + let x0 = (frame_index * move_step) % (width - block_size); + let y0 = (frame_index * move_step) % (height - block_size); + // Fill the block with random colors + for y in 0..block_size { + for x in 0..block_size { + let index = (((y0 + y) * width + x0 + x) * 4) as usize; + if index + 3 < bgra.len() { + let noise = rng.gen_range(0..255) as f32 / 255.0; + let value = (255.0 * gradient + noise * 50.0) as u8; + bgra[index] = value; + bgra[index + 1] = value; + bgra[index + 2] = value; + bgra[index + 3] = 255; + } + } + } + let dst_stride_y = dst_fmt.stride[0]; + let dst_stride_uv = dst_fmt.stride[1]; + let mut dst = vec![0u8; (dst_fmt.h * dst_stride_y * 2) as usize]; + let dst_y = dst.as_mut_ptr(); + let dst_u = dst[dst_fmt.u..].as_mut_ptr(); + let dst_v = dst[dst_fmt.v..].as_mut_ptr(); + let res = unsafe { + crate::ARGBToI420( + bgra.as_ptr(), + (width * 4) as _, + dst_y, + dst_stride_y as _, + dst_u, + dst_stride_uv as _, + dst_v, + dst_stride_uv as _, + width as _, + height as _, + ) + }; + if res != 0 { + bail!("ARGBToI420 failed: {}", res); + } + Ok(dst) + }; let Ok(mut av1) = AomEncoder::new( EncoderCfg::AOM(AomEncoderConfig { width, @@ -1012,52 +1064,39 @@ pub fn test_av1() { ) else { return false; }; - let Ok(mut vp9) = VpxEncoder::new( - EncoderCfg::VPX(VpxEncoderConfig { - codec: VpxVideoCodecId::VP9, - width, - height, - quality, - keyframe_interval, - }), - i444, - ) else { - return true; - }; - let frame_count = 10; - let mut yuv = vec![0u8; (width * height * 3 / 2) as usize]; - let start = Instant::now(); + let mut key_frame_time = Duration::ZERO; + let mut non_key_frame_time_sum = Duration::ZERO; + let pts = Instant::now(); + let yuvfmt = av1.yuvfmt(); for i in 0..frame_count { - for w in 0..width { - yuv[w as usize] = i as u8; - } - if av1.encode(0, &yuv, super::STRIDE_ALIGN).is_err() { + let Ok(yuv) = generate_fake_data(i, yuvfmt.clone()) else { + return false; + }; + let start = Instant::now(); + if av1 + .encode(pts.elapsed().as_millis() as _, &yuv, super::STRIDE_ALIGN) + .is_err() + { log::debug!("av1 encode failed"); if i == 0 { return false; } } - } - let av1_time = start.elapsed(); - log::info!("av1 time: {:?}", av1_time); - if av1_time < std::time::Duration::from_millis(250) { - return true; - } - let start = Instant::now(); - for i in 0..frame_count { - for w in 0..width { - yuv[w as usize] = i as u8; - } - if vp9.encode(0, &yuv, super::STRIDE_ALIGN).is_err() { - log::debug!("vp9 encode failed"); - if i == 0 { - return true; - } + if i == 0 { + key_frame_time = start.elapsed(); + } else { + non_key_frame_time_sum += start.elapsed(); } } - let vp9_time = start.elapsed(); - log::info!("vp9 time: {:?}", vp9_time); - av1_time * 2 / 3 < vp9_time + let non_key_frame_time = non_key_frame_time_sum / (frame_count - 1); + log::info!( + "av1 time: key: {:?}, non-key: {:?}, consume: {:?}", + key_frame_time, + non_key_frame_time, + pts.elapsed() + ); + key_frame_time < Duration::from_millis(90) + && non_key_frame_time < Duration::from_millis(30) }; std::thread::spawn(move || { let v = f(); From 8b710f62c80c4ca93f36fd31389a39b6ba08ce2c Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Mon, 18 Nov 2024 15:43:41 +0800 Subject: [PATCH 386/541] feat: android clipboard, multi-formats (#9950) * feat: android clipboard, multi-formats Signed-off-by: fufesou * Chore Signed-off-by: fufesou * Remove unused code Signed-off-by: fufesou --------- Signed-off-by: fufesou --- .../flutter_hbb/FloatingWindowService.kt | 16 +- .../com/carriez/flutter_hbb/MainActivity.kt | 23 ++ .../carriez/flutter_hbb/RdClipboardManager.kt | 224 ++++++++++++++++++ flutter/android/app/src/main/kotlin/ffi.kt | 6 +- flutter/lib/mobile/pages/server_page.dart | 4 +- flutter/lib/models/server_model.dart | 15 ++ libs/scrap/src/android/ffi.rs | 114 ++++++++- src/client.rs | 48 +++- src/client/io_loop.rs | 25 +- src/clipboard.rs | 78 +++++- src/flutter.rs | 8 +- src/flutter_ffi.rs | 18 +- src/lang/ar.rs | 1 + src/lang/be.rs | 1 + src/lang/bg.rs | 1 + src/lang/ca.rs | 1 + src/lang/cn.rs | 1 + src/lang/cs.rs | 1 + src/lang/da.rs | 1 + src/lang/de.rs | 1 + src/lang/el.rs | 1 + src/lang/eo.rs | 1 + src/lang/es.rs | 1 + src/lang/et.rs | 1 + src/lang/eu.rs | 1 + src/lang/fa.rs | 1 + src/lang/fr.rs | 1 + src/lang/he.rs | 1 + src/lang/hr.rs | 1 + src/lang/hu.rs | 1 + src/lang/id.rs | 1 + src/lang/it.rs | 1 + src/lang/ja.rs | 1 + src/lang/ko.rs | 1 + src/lang/kz.rs | 1 + src/lang/lt.rs | 1 + src/lang/lv.rs | 1 + src/lang/nb.rs | 1 + src/lang/nl.rs | 1 + src/lang/pl.rs | 1 + src/lang/pt_PT.rs | 1 + src/lang/ptbr.rs | 1 + src/lang/ro.rs | 1 + src/lang/ru.rs | 1 + src/lang/sk.rs | 1 + src/lang/sl.rs | 1 + src/lang/sq.rs | 1 + src/lang/sr.rs | 1 + src/lang/sv.rs | 1 + src/lang/template.rs | 1 + src/lang/th.rs | 1 + src/lang/tr.rs | 1 + src/lang/tw.rs | 1 + src/lang/uk.rs | 1 + src/lang/vn.rs | 1 + src/lib.rs | 2 +- src/server.rs | 19 +- src/server/clipboard_service.rs | 41 +++- src/server/connection.rs | 13 +- src/ui_cm_interface.rs | 11 + src/ui_session_interface.rs | 7 +- 61 files changed, 669 insertions(+), 46 deletions(-) create mode 100644 flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/RdClipboardManager.kt diff --git a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/FloatingWindowService.kt b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/FloatingWindowService.kt index e117f5b9fdae..c5da81c7c4db 100644 --- a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/FloatingWindowService.kt +++ b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/FloatingWindowService.kt @@ -304,7 +304,13 @@ class FloatingWindowService : Service(), View.OnTouchListener { val popupMenu = PopupMenu(this, floatingView) val idShowRustDesk = 0 popupMenu.menu.add(0, idShowRustDesk, 0, translate("Show RustDesk")) - val idStopService = 1 + // For host side, clipboard sync + val idSyncClipboard = 1 + val isClipboardListenerEnabled = MainActivity.rdClipboardManager?.isListening ?: false + if (isClipboardListenerEnabled) { + popupMenu.menu.add(0, idSyncClipboard, 0, translate("Update client clipboard")) + } + val idStopService = 2 popupMenu.menu.add(0, idStopService, 0, translate("Stop service")) popupMenu.setOnMenuItemClickListener { menuItem -> when (menuItem.itemId) { @@ -312,6 +318,10 @@ class FloatingWindowService : Service(), View.OnTouchListener { openMainActivity() true } + idSyncClipboard -> { + syncClipboard() + true + } idStopService -> { stopMainService() true @@ -340,6 +350,10 @@ class FloatingWindowService : Service(), View.OnTouchListener { } } + private fun syncClipboard() { + MainActivity.rdClipboardManager?.syncClipboard(false) + } + private fun stopMainService() { MainActivity.flutterMethodChannel?.invokeMethod("stop_service", null) } diff --git a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainActivity.kt b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainActivity.kt index 6b197a39f8ae..15dee6002f57 100644 --- a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainActivity.kt +++ b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainActivity.kt @@ -13,6 +13,8 @@ import android.content.ComponentName import android.content.Context import android.content.Intent import android.content.ServiceConnection +import android.content.ClipboardManager +import android.os.Bundle import android.os.Build import android.os.IBinder import android.util.Log @@ -40,6 +42,9 @@ import kotlin.concurrent.thread class MainActivity : FlutterActivity() { companion object { var flutterMethodChannel: MethodChannel? = null + private var _rdClipboardManager: RdClipboardManager? = null + val rdClipboardManager: RdClipboardManager? + get() = _rdClipboardManager; } private val channelTag = "mChannel" @@ -90,11 +95,20 @@ class MainActivity : FlutterActivity() { } } + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + if (_rdClipboardManager == null) { + _rdClipboardManager = RdClipboardManager(getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager) + FFI.setClipboardManager(_rdClipboardManager!!) + } + } + override fun onDestroy() { Log.e(logTag, "onDestroy") mainService?.let { unbindService(serviceConnection) } + rdClipboardManager?.rustEnableServiceClipboard(false) super.onDestroy() } @@ -393,6 +407,15 @@ class MainActivity : FlutterActivity() { super.onStart() stopService(Intent(this, FloatingWindowService::class.java)) } + + // For client side + // When swithing from other app to this app, try to sync clipboard. + override fun onWindowFocusChanged(hasFocus: Boolean) { + super.onWindowFocusChanged(hasFocus) + if (hasFocus) { + rdClipboardManager?.syncClipboard(true) + } + } } // https://cjycode.com/flutter_rust_bridge/guides/how-to/ndk-init diff --git a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/RdClipboardManager.kt b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/RdClipboardManager.kt new file mode 100644 index 000000000000..0e098cb0831b --- /dev/null +++ b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/RdClipboardManager.kt @@ -0,0 +1,224 @@ +package com.carriez.flutter_hbb + +import java.nio.ByteBuffer +import java.util.Timer +import java.util.TimerTask + +import android.content.ClipData +import android.content.ClipDescription +import android.content.ClipboardManager +import android.util.Log +import androidx.annotation.Keep + +import hbb.MessageOuterClass.ClipboardFormat +import hbb.MessageOuterClass.Clipboard +import hbb.MessageOuterClass.MultiClipboards + +import ffi.FFI + +class RdClipboardManager(private val clipboardManager: ClipboardManager) { + private val logTag = "RdClipboardManager" + private val supportedMimeTypes = arrayOf( + ClipDescription.MIMETYPE_TEXT_PLAIN, + ClipDescription.MIMETYPE_TEXT_HTML + ) + + // 1. Avoid listening to the same clipboard data updated by `rustUpdateClipboard`. + // 2. Avoid sending the clipboard data before enabling client clipboard. + // 1) Disable clipboard + // 2) Copy text "a" + // 3) Enable clipboard + // 4) Switch to another app + // 5) Switch back to the app + // 6) "a" should not be sent to the client, because it's copied before enabling clipboard + // + // It's okay to that `rustEnableClientClipboard(false)` is called after `rustUpdateClipboard`, + // though the `lastUpdatedClipData` will be set to null once. + private var lastUpdatedClipData: ClipData? = null + private var isClientEnabled = true; + private var _isListening = false; + val isListening: Boolean + get() = _isListening + + fun checkPrimaryClip(isClient: Boolean, isSync: Boolean) { + val clipData = clipboardManager.primaryClip + if (clipData != null && clipData.itemCount > 0) { + // Only handle the first item in the clipboard for now. + val clip = clipData.getItemAt(0) + val isHostSync = !isClient && isSync + // Ignore the `isClipboardDataEqual()` check if it's a host sync operation. + // Because it's a action manually triggered by the user. + if (!isHostSync) { + if (lastUpdatedClipData != null && isClipboardDataEqual(clipData, lastUpdatedClipData!!)) { + Log.d(logTag, "Clipboard data is the same as last update, ignore") + return + } + } + val mimeTypeCount = clipData.description.getMimeTypeCount() + val mimeTypes = mutableListOf() + for (i in 0 until mimeTypeCount) { + mimeTypes.add(clipData.description.getMimeType(i)) + } + var text: CharSequence? = null; + var html: String? = null; + if (isSupportedMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN)) { + text = clip?.text + } + if (isSupportedMimeType(ClipDescription.MIMETYPE_TEXT_HTML)) { + text = clip?.text + html = clip?.htmlText + } + var count = 0 + val clips = MultiClipboards.newBuilder() + if (text != null) { + val content = com.google.protobuf.ByteString.copyFromUtf8(text.toString()) + clips.addClipboards(Clipboard.newBuilder().setFormat(ClipboardFormat.Text).setContent(content).build()) + count++ + } + if (html != null) { + val content = com.google.protobuf.ByteString.copyFromUtf8(html) + clips.addClipboards(Clipboard.newBuilder().setFormat(ClipboardFormat.Html).setContent(content).build()) + count++ + } + if (count > 0) { + val clipsBytes = clips.build().toByteArray() + val isClientFlag = if (isClient) 1 else 0 + val clipsBuf = ByteBuffer.allocateDirect(clipsBytes.size + 1).apply { + put(isClientFlag.toByte()) + put(clipsBytes) + } + clipsBuf.flip() + lastUpdatedClipData = clipData + Log.d(logTag, "${if (isClient) "client" else "host"}, send clipboard data to the remote") + FFI.onClipboardUpdate(clipsBuf) + } + } + } + + private val clipboardListener = object : ClipboardManager.OnPrimaryClipChangedListener { + override fun onPrimaryClipChanged() { + Log.d(logTag, "onPrimaryClipChanged") + checkPrimaryClip(true, false) + } + } + + private fun isSupportedMimeType(mimeType: String): Boolean { + return supportedMimeTypes.contains(mimeType) + } + + private fun isClipboardDataEqual(left: ClipData, right: ClipData): Boolean { + if (left.description.getMimeTypeCount() != right.description.getMimeTypeCount()) { + return false + } + val mimeTypeCount = left.description.getMimeTypeCount() + for (i in 0 until mimeTypeCount) { + if (left.description.getMimeType(i) != right.description.getMimeType(i)) { + return false + } + } + + if (left.itemCount != right.itemCount) { + return false + } + for (i in 0 until left.itemCount) { + val mimeType = left.description.getMimeType(i) + if (!isSupportedMimeType(mimeType)) { + continue + } + val leftItem = left.getItemAt(i) + val rightItem = right.getItemAt(i) + if (mimeType == ClipDescription.MIMETYPE_TEXT_PLAIN || mimeType == ClipDescription.MIMETYPE_TEXT_HTML) { + if (leftItem.text != rightItem.text || leftItem.htmlText != rightItem.htmlText) { + return false + } + } + } + return true + } + + @Keep + fun rustEnableServiceClipboard(enable: Boolean) { + Log.d(logTag, "rustEnableServiceClipboard: enable: $enable, _isListening: $_isListening") + if (enable) { + if (!_isListening) { + clipboardManager.addPrimaryClipChangedListener(clipboardListener) + _isListening = true + } + } else { + if (_isListening) { + clipboardManager.removePrimaryClipChangedListener(clipboardListener) + _isListening = false + lastUpdatedClipData = null + } + } + } + + @Keep + fun rustEnableClientClipboard(enable: Boolean) { + Log.d(logTag, "rustEnableClientClipboard: enable: $enable") + isClientEnabled = enable + if (enable) { + lastUpdatedClipData = clipboardManager.primaryClip + } else { + lastUpdatedClipData = null + } + } + + fun syncClipboard(isClient: Boolean) { + Log.d(logTag, "syncClipboard: isClient: $isClient, isClientEnabled: $isClientEnabled, _isListening: $_isListening") + if (isClient && !isClientEnabled) { + return + } + if (!isClient && !_isListening) { + return + } + checkPrimaryClip(isClient, true) + } + + @Keep + fun rustUpdateClipboard(clips: ByteArray) { + val clips = MultiClipboards.parseFrom(clips) + var mimeTypes = mutableListOf() + var text: String? = null + var html: String? = null + for (clip in clips.getClipboardsList()) { + when (clip.format) { + ClipboardFormat.Text -> { + mimeTypes.add(ClipDescription.MIMETYPE_TEXT_PLAIN) + text = String(clip.content.toByteArray(), Charsets.UTF_8) + } + ClipboardFormat.Html -> { + mimeTypes.add(ClipDescription.MIMETYPE_TEXT_HTML) + html = String(clip.content.toByteArray(), Charsets.UTF_8) + } + ClipboardFormat.ImageRgba -> { + } + ClipboardFormat.ImagePng -> { + } + else -> { + Log.e(logTag, "Unsupported clipboard format: ${clip.format}") + } + } + } + + val clipDescription = ClipDescription("clipboard", mimeTypes.toTypedArray()) + var item: ClipData.Item? = null + if (text == null) { + Log.e(logTag, "No text content in clipboard") + return + } else { + if (html == null) { + item = ClipData.Item(text) + } else { + item = ClipData.Item(text, html) + } + } + if (item == null) { + Log.e(logTag, "No item in clipboard") + return + } + val clipData = ClipData(clipDescription, item) + lastUpdatedClipData = clipData + clipboardManager.setPrimaryClip(clipData) + } +} diff --git a/flutter/android/app/src/main/kotlin/ffi.kt b/flutter/android/app/src/main/kotlin/ffi.kt index 43368c1e6d4b..653465782b3c 100644 --- a/flutter/android/app/src/main/kotlin/ffi.kt +++ b/flutter/android/app/src/main/kotlin/ffi.kt @@ -5,6 +5,8 @@ package ffi import android.content.Context import java.nio.ByteBuffer +import com.carriez.flutter_hbb.RdClipboardManager + object FFI { init { System.loadLibrary("rustdesk") @@ -12,6 +14,7 @@ object FFI { external fun init(ctx: Context) external fun initContext(ctx: Context) + external fun setClipboardManager(clipboardManager: RdClipboardManager) external fun startServer(app_dir: String, custom_client_config: String) external fun startService() external fun onVideoFrameUpdate(buf: ByteBuffer) @@ -21,4 +24,5 @@ object FFI { external fun setFrameRawEnable(name: String, value: Boolean) external fun setCodecInfo(info: String) external fun getLocalOption(key: String): String -} \ No newline at end of file + external fun onClipboardUpdate(clips: ByteBuffer) +} diff --git a/flutter/lib/mobile/pages/server_page.dart b/flutter/lib/mobile/pages/server_page.dart index d6710b43d6a3..db91e998b6ea 100644 --- a/flutter/lib/mobile/pages/server_page.dart +++ b/flutter/lib/mobile/pages/server_page.dart @@ -596,7 +596,9 @@ class _PermissionCheckerState extends State { translate("android_version_audio_tip"), style: const TextStyle(color: MyTheme.darkGray), )) - ]) + ]), + PermissionRow(translate("Enable clipboard"), serverModel.clipboardOk, + serverModel.toggleClipboard), ])); } } diff --git a/flutter/lib/models/server_model.dart b/flutter/lib/models/server_model.dart index 1d800ef69678..7754672c0d0e 100644 --- a/flutter/lib/models/server_model.dart +++ b/flutter/lib/models/server_model.dart @@ -30,6 +30,7 @@ class ServerModel with ChangeNotifier { bool _inputOk = false; bool _audioOk = false; bool _fileOk = false; + bool _clipboardOk = false; bool _showElevation = false; bool hideCm = false; int _connectStatus = 0; // Rendezvous Server status @@ -59,6 +60,8 @@ class ServerModel with ChangeNotifier { bool get fileOk => _fileOk; + bool get clipboardOk => _clipboardOk; + bool get showElevation => _showElevation; int get connectStatus => _connectStatus; @@ -209,6 +212,10 @@ class ServerModel with ChangeNotifier { _fileOk = fileOption != 'N'; } + // clipboard + final clipOption = await bind.mainGetOption(key: kOptionEnableClipboard); + _clipboardOk = clipOption != 'N'; + notifyListeners(); } @@ -315,6 +322,14 @@ class ServerModel with ChangeNotifier { notifyListeners(); } + toggleClipboard() async { + _clipboardOk = !_clipboardOk; + bind.mainSetOption( + key: kOptionEnableClipboard, + value: _clipboardOk ? defaultOptionYes : 'N'); + notifyListeners(); + } + toggleInput() async { if (clients.isNotEmpty) { await showClientsMayNotBeChangedAlert(parent.target); diff --git a/libs/scrap/src/android/ffi.rs b/libs/scrap/src/android/ffi.rs index 4608597ce6d7..0e48f60e6f72 100644 --- a/libs/scrap/src/android/ffi.rs +++ b/libs/scrap/src/android/ffi.rs @@ -5,9 +5,11 @@ use jni::sys::jboolean; use jni::JNIEnv; use jni::{ objects::{GlobalRef, JClass, JObject}, + strings::JNIString, JavaVM, }; +use hbb_common::{message_proto::MultiClipboards, protobuf::Message}; use jni::errors::{Error as JniError, Result as JniResult}; use lazy_static::lazy_static; use serde::Deserialize; @@ -16,6 +18,7 @@ use std::os::raw::c_void; use std::sync::atomic::{AtomicPtr, Ordering::SeqCst}; use std::sync::{Mutex, RwLock}; use std::time::{Duration, Instant}; + lazy_static! { static ref JVM: RwLock> = RwLock::new(None); static ref MAIN_SERVICE_CTX: RwLock> = RwLock::new(None); // MainService -> video service / audio service / info @@ -23,6 +26,9 @@ lazy_static! { static ref AUDIO_RAW: Mutex = Mutex::new(FrameRaw::new("audio", MAX_AUDIO_FRAME_TIMEOUT)); static ref NDK_CONTEXT_INITED: Mutex = Default::default(); static ref MEDIA_CODEC_INFOS: RwLock> = RwLock::new(None); + static ref CLIPBOARD_MANAGER: RwLock> = RwLock::new(None); + static ref CLIPBOARDS_HOST: Mutex> = Mutex::new(None); + static ref CLIPBOARDS_CLIENT: Mutex> = Mutex::new(None); } const MAX_VIDEO_FRAME_TIMEOUT: Duration = Duration::from_millis(100); @@ -105,6 +111,14 @@ pub fn get_audio_raw<'a>(dst: &mut Vec, last: &mut Vec) -> Option<()> { AUDIO_RAW.lock().ok()?.take(dst, last) } +pub fn get_clipboards(client: bool) -> Option { + if client { + CLIPBOARDS_CLIENT.lock().ok()?.take() + } else { + CLIPBOARDS_HOST.lock().ok()?.take() + } +} + #[no_mangle] pub extern "system" fn Java_ffi_FFI_onVideoFrameUpdate( env: JNIEnv, @@ -133,6 +147,27 @@ pub extern "system" fn Java_ffi_FFI_onAudioFrameUpdate( } } +#[no_mangle] +pub extern "system" fn Java_ffi_FFI_onClipboardUpdate( + env: JNIEnv, + _class: JClass, + buffer: JByteBuffer, +) { + if let Ok(data) = env.get_direct_buffer_address(&buffer) { + if let Ok(len) = env.get_direct_buffer_capacity(&buffer) { + let data = unsafe { std::slice::from_raw_parts(data, len) }; + if let Ok(clips) = MultiClipboards::parse_from_bytes(&data[1..]) { + let is_client = data[0] == 1; + if is_client { + *CLIPBOARDS_CLIENT.lock().unwrap() = Some(clips); + } else { + *CLIPBOARDS_HOST.lock().unwrap() = Some(clips); + } + } + } + } +} + #[no_mangle] pub extern "system" fn Java_ffi_FFI_setFrameRawEnable( env: JNIEnv, @@ -157,7 +192,11 @@ pub extern "system" fn Java_ffi_FFI_init(env: JNIEnv, _class: JClass, ctx: JObje log::debug!("MainService init from java"); if let Ok(jvm) = env.get_java_vm() { let java_vm = jvm.get_java_vm_pointer() as *mut c_void; - *JVM.write().unwrap() = Some(jvm); + let mut jvm_lock = JVM.write().unwrap(); + if jvm_lock.is_none() { + *jvm_lock = Some(jvm); + } + drop(jvm_lock); if let Ok(context) = env.new_global_ref(ctx) { let context_jobject = context.as_obj().as_raw() as *mut c_void; *MAIN_SERVICE_CTX.write().unwrap() = Some(context); @@ -178,6 +217,26 @@ pub extern "system" fn Java_ffi_FFI_initContext(env: JNIEnv, _class: JClass, ctx } } +#[no_mangle] +pub extern "system" fn Java_ffi_FFI_setClipboardManager( + env: JNIEnv, + _class: JClass, + clipboard_manager: JObject, +) { + log::debug!("ClipboardManager init from java"); + if let Ok(jvm) = env.get_java_vm() { + let java_vm = jvm.get_java_vm_pointer() as *mut c_void; + let mut jvm_lock = JVM.write().unwrap(); + if jvm_lock.is_none() { + *jvm_lock = Some(jvm); + } + drop(jvm_lock); + if let Ok(manager) = env.new_global_ref(clipboard_manager) { + *CLIPBOARD_MANAGER.write().unwrap() = Some(manager); + } + } +} + #[derive(Debug, Deserialize, Clone)] pub struct MediaCodecInfo { pub name: String, @@ -287,6 +346,59 @@ pub fn call_main_service_key_event(data: &[u8]) -> JniResult<()> { } } +fn _call_clipboard_manager(name: S, sig: T, args: &[JValue]) -> JniResult<()> +where + S: Into, + T: Into + AsRef, +{ + if let (Some(jvm), Some(cm)) = ( + JVM.read().unwrap().as_ref(), + CLIPBOARD_MANAGER.read().unwrap().as_ref(), + ) { + let mut env = jvm.attach_current_thread()?; + env.call_method(cm, name, sig, args)?; + return Ok(()); + } else { + return Err(JniError::ThrowFailed(-1)); + } +} + +pub fn call_clipboard_manager_update_clipboard(data: &[u8]) -> JniResult<()> { + if let (Some(jvm), Some(cm)) = ( + JVM.read().unwrap().as_ref(), + CLIPBOARD_MANAGER.read().unwrap().as_ref(), + ) { + let mut env = jvm.attach_current_thread()?; + let data = env.byte_array_from_slice(data)?; + + env.call_method( + cm, + "rustUpdateClipboard", + "([B)V", + &[JValue::Object(&JObject::from(data))], + )?; + return Ok(()); + } else { + return Err(JniError::ThrowFailed(-1)); + } +} + +pub fn call_clipboard_manager_enable_service_clipboard(enable: bool) -> JniResult<()> { + _call_clipboard_manager( + "rustEnableServiceClipboard", + "(Z)V", + &[JValue::Bool(jboolean::from(enable))], + ) +} + +pub fn call_clipboard_manager_enable_client_clipboard(enable: bool) -> JniResult<()> { + _call_clipboard_manager( + "rustEnableClientClipboard", + "(Z)V", + &[JValue::Bool(jboolean::from(enable))], + ) +} + pub fn call_main_service_get_by_name(name: &str) -> JniResult { if let (Some(jvm), Some(ctx)) = ( JVM.read().unwrap().as_ref(), diff --git a/src/client.rs b/src/client.rs index 2abc4d0ff017..c36063530fce 100644 --- a/src/client.rs +++ b/src/client.rs @@ -71,8 +71,10 @@ use crate::{ ui_session_interface::{InvokeUiSession, Session}, }; +#[cfg(not(target_os = "ios"))] +use crate::clipboard::CLIPBOARD_INTERVAL; #[cfg(not(any(target_os = "android", target_os = "ios")))] -use crate::clipboard::{check_clipboard, ClipboardSide, CLIPBOARD_INTERVAL}; +use crate::clipboard::{check_clipboard, ClipboardSide}; #[cfg(not(feature = "flutter"))] #[cfg(not(any(target_os = "android", target_os = "ios")))] use crate::ui_session_interface::SessionPermissionConfig; @@ -131,7 +133,7 @@ pub(crate) struct ClientClipboardContext { /// Client of the remote desktop. pub struct Client; -#[cfg(not(any(target_os = "android", target_os = "ios")))] +#[cfg(not(target_os = "ios"))] struct TextClipboardState { is_required: bool, running: bool, @@ -144,6 +146,10 @@ lazy_static::lazy_static! { #[cfg(not(any(target_os = "android", target_os = "ios")))] lazy_static::lazy_static! { static ref ENIGO: Arc> = Arc::new(Mutex::new(enigo::Enigo::new())); +} + +#[cfg(not(target_os = "ios"))] +lazy_static::lazy_static! { static ref TEXT_CLIPBOARD_STATE: Arc> = Arc::new(Mutex::new(TextClipboardState::new())); } @@ -648,12 +654,12 @@ impl Client { #[inline] #[cfg(feature = "flutter")] - #[cfg(not(any(target_os = "android", target_os = "ios")))] + #[cfg(not(target_os = "ios"))] pub fn set_is_text_clipboard_required(b: bool) { TEXT_CLIPBOARD_STATE.lock().unwrap().is_required = b; } - #[cfg(not(any(target_os = "android", target_os = "ios")))] + #[cfg(not(target_os = "ios"))] fn try_stop_clipboard() { // There's a bug here. // If session is closed by the peer, `has_sessions_running()` will always return true. @@ -748,9 +754,41 @@ impl Client { Some(rx_started) } + + #[cfg(target_os = "android")] + fn try_start_clipboard(_p: Option<()>) -> Option> { + let mut clipboard_lock = TEXT_CLIPBOARD_STATE.lock().unwrap(); + if clipboard_lock.running { + return None; + } + clipboard_lock.running = true; + + log::info!("Start text clipboard loop"); + std::thread::spawn(move || { + loop { + if !TEXT_CLIPBOARD_STATE.lock().unwrap().running { + break; + } + if !TEXT_CLIPBOARD_STATE.lock().unwrap().is_required { + std::thread::sleep(Duration::from_millis(CLIPBOARD_INTERVAL)); + continue; + } + + if let Some(msg) = crate::clipboard::get_clipboards_msg(true) { + crate::flutter::send_text_clipboard_msg(msg); + } + + std::thread::sleep(Duration::from_millis(CLIPBOARD_INTERVAL)); + } + log::info!("Stop text clipboard loop"); + TEXT_CLIPBOARD_STATE.lock().unwrap().running = false; + }); + + None + } } -#[cfg(not(any(target_os = "android", target_os = "ios")))] +#[cfg(not(target_os = "ios"))] impl TextClipboardState { fn new() -> Self { Self { diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index cc74c96edd14..9aec89dd3230 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -8,9 +8,9 @@ use std::{ }; #[cfg(not(any(target_os = "android", target_os = "ios")))] -use crate::clipboard::{update_clipboard, ClipboardSide, CLIPBOARD_INTERVAL}; +use crate::clipboard::{update_clipboard, ClipboardSide}; #[cfg(not(any(target_os = "ios")))] -use crate::{audio_service, ConnInner, CLIENT_SERVER}; +use crate::{audio_service, clipboard::CLIPBOARD_INTERVAL, ConnInner, CLIENT_SERVER}; use crate::{ client::{ self, new_voice_call_request, Client, Data, Interface, MediaData, MediaSender, @@ -302,7 +302,7 @@ impl Remote { .unwrap() .set_disconnected(round); - #[cfg(not(any(target_os = "android", target_os = "ios")))] + #[cfg(not(target_os = "ios"))] if _set_disconnected_ok { Client::try_stop_clipboard(); } @@ -1177,7 +1177,7 @@ impl Remote { self.check_clipboard_file_context(); if !(self.handler.is_file_transfer() || self.handler.is_port_forward()) { #[cfg(feature = "flutter")] - #[cfg(not(any(target_os = "android", target_os = "ios")))] + #[cfg(not(target_os = "ios"))] let rx = Client::try_start_clipboard(None); #[cfg(not(feature = "flutter"))] #[cfg(not(any(target_os = "android", target_os = "ios")))] @@ -1188,7 +1188,7 @@ impl Remote { }, )); // To make sure current text clipboard data is updated. - #[cfg(not(any(target_os = "android", target_os = "ios")))] + #[cfg(not(target_os = "ios"))] if let Some(mut rx) = rx { timeout(CLIPBOARD_INTERVAL, rx.recv()).await.ok(); } @@ -1209,6 +1209,11 @@ impl Remote { }); } } + // to-do: Android, is `sync_init_clipboard` really needed? + // https://github.com/rustdesk/rustdesk/discussions/9010 + + #[cfg(target_os = "android")] + crate::flutter::update_text_clipboard_required(); // on connection established client #[cfg(all(feature = "flutter", feature = "plugin_framework"))] @@ -1240,7 +1245,7 @@ impl Remote { if !self.handler.lc.read().unwrap().disable_clipboard.v { #[cfg(not(any(target_os = "android", target_os = "ios")))] update_clipboard(vec![cb], ClipboardSide::Client); - #[cfg(any(target_os = "android", target_os = "ios"))] + #[cfg(target_os = "ios")] { let content = if cb.compress { hbb_common::compress::decompress(&cb.content) @@ -1251,12 +1256,16 @@ impl Remote { self.handler.clipboard(content); } } + #[cfg(target_os = "android")] + crate::clipboard::handle_msg_clipboard(cb); } } Some(message::Union::MultiClipboards(_mcb)) => { if !self.handler.lc.read().unwrap().disable_clipboard.v { #[cfg(not(any(target_os = "android", target_os = "ios")))] update_clipboard(_mcb.clipboards, ClipboardSide::Client); + #[cfg(target_os = "android")] + crate::clipboard::handle_msg_multi_clipboards(_mcb); } } #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] @@ -1421,14 +1430,14 @@ impl Remote { Ok(Permission::Keyboard) => { *self.handler.server_keyboard_enabled.write().unwrap() = p.enabled; #[cfg(feature = "flutter")] - #[cfg(not(any(target_os = "android", target_os = "ios")))] + #[cfg(not(target_os = "ios"))] crate::flutter::update_text_clipboard_required(); self.handler.set_permission("keyboard", p.enabled); } Ok(Permission::Clipboard) => { *self.handler.server_clipboard_enabled.write().unwrap() = p.enabled; #[cfg(feature = "flutter")] - #[cfg(not(any(target_os = "android", target_os = "ios")))] + #[cfg(not(target_os = "ios"))] crate::flutter::update_text_clipboard_required(); self.handler.set_permission("clipboard", p.enabled); } diff --git a/src/clipboard.rs b/src/clipboard.rs index 329b392bba72..ac3a83f00f72 100644 --- a/src/clipboard.rs +++ b/src/clipboard.rs @@ -1,4 +1,6 @@ +#[cfg(not(target_os = "android"))] use arboard::{ClipboardData, ClipboardFormat}; +#[cfg(not(target_os = "android"))] use clipboard_master::{ClipboardHandler, Master, Shutdown}; use hbb_common::{bail, log, message_proto::*, ResultType}; use std::{ @@ -16,6 +18,7 @@ const RUSTDESK_CLIPBOARD_OWNER_FORMAT: &'static str = "dyn.com.rustdesk.owner"; // Add special format for Excel XML Spreadsheet const CLIPBOARD_FORMAT_EXCEL_XML_SPREADSHEET: &'static str = "XML Spreadsheet"; +#[cfg(not(target_os = "android"))] lazy_static::lazy_static! { static ref ARBOARD_MTX: Arc> = Arc::new(Mutex::new(())); // cache the clipboard msg @@ -27,9 +30,12 @@ lazy_static::lazy_static! { static ref CLIPBOARD_CTX: Arc>> = Arc::new(Mutex::new(None)); } +#[cfg(not(target_os = "android"))] const CLIPBOARD_GET_MAX_RETRY: usize = 3; +#[cfg(not(target_os = "android"))] const CLIPBOARD_GET_RETRY_INTERVAL_DUR: Duration = Duration::from_millis(33); +#[cfg(not(target_os = "android"))] const SUPPORTED_FORMATS: &[ClipboardFormat] = &[ ClipboardFormat::Text, ClipboardFormat::Html, @@ -146,6 +152,7 @@ impl ClipboardContext { } } +#[cfg(not(target_os = "android"))] pub fn check_clipboard( ctx: &mut Option, side: ClipboardSide, @@ -194,6 +201,7 @@ pub fn check_clipboard_cm() -> ResultType { } } +#[cfg(not(target_os = "android"))] fn update_clipboard_(multi_clipboards: Vec, side: ClipboardSide) { let mut to_update_data = proto::from_multi_clipbards(multi_clipboards); if to_update_data.is_empty() { @@ -224,17 +232,20 @@ fn update_clipboard_(multi_clipboards: Vec, side: ClipboardSide) { } } +#[cfg(not(target_os = "android"))] pub fn update_clipboard(multi_clipboards: Vec, side: ClipboardSide) { std::thread::spawn(move || { update_clipboard_(multi_clipboards, side); }); } +#[cfg(not(target_os = "android"))] #[cfg(not(any(all(target_os = "linux", feature = "unix-file-copy-paste"))))] pub struct ClipboardContext { inner: arboard::Clipboard, } +#[cfg(not(target_os = "android"))] #[cfg(not(any(all(target_os = "linux", feature = "unix-file-copy-paste"))))] #[allow(unreachable_code)] impl ClipboardContext { @@ -337,10 +348,20 @@ impl ClipboardContext { pub fn is_support_multi_clipboard(peer_version: &str, peer_platform: &str) -> bool { use hbb_common::get_version_number; - get_version_number(peer_version) >= get_version_number("1.3.0") - && !["", "Android", &whoami::Platform::Ios.to_string()].contains(&peer_platform) + if get_version_number(peer_version) < get_version_number("1.3.0") { + return false; + } + if ["", &whoami::Platform::Ios.to_string()].contains(&peer_platform) { + return false; + } + if "Android" == peer_platform && get_version_number(peer_version) < get_version_number("1.3.3") + { + return false; + } + true } +#[cfg(not(target_os = "android"))] pub fn get_current_clipboard_msg( peer_version: &str, peer_platform: &str, @@ -406,6 +427,7 @@ impl std::fmt::Display for ClipboardSide { } } +#[cfg(not(target_os = "android"))] pub fn start_clipbard_master_thread( handler: impl ClipboardHandler + Send + 'static, tx_start_res: Sender<(Option, String)>, @@ -437,6 +459,7 @@ pub fn start_clipbard_master_thread( pub use proto::get_msg_if_not_support_multi_clip; mod proto { + #[cfg(not(target_os = "android"))] use arboard::ClipboardData; use hbb_common::{ compress::{compress as compress_func, decompress}, @@ -459,6 +482,7 @@ mod proto { } } + #[cfg(not(target_os = "android"))] fn image_to_proto(a: arboard::ImageData) -> Clipboard { match &a { arboard::ImageData::Rgba(rgba) => { @@ -519,6 +543,7 @@ mod proto { } } + #[cfg(not(target_os = "android"))] fn clipboard_data_to_proto(data: ClipboardData) -> Option { let d = match data { ClipboardData::Text(s) => plain_to_proto(s, ClipboardFormat::Text), @@ -531,6 +556,7 @@ mod proto { Some(d) } + #[cfg(not(target_os = "android"))] pub fn create_multi_clipboards(vec_data: Vec) -> MultiClipboards { MultiClipboards { clipboards: vec_data @@ -541,6 +567,7 @@ mod proto { } } + #[cfg(not(target_os = "android"))] fn from_clipboard(clipboard: Clipboard) -> Option { let data = if clipboard.compress { decompress(&clipboard.content) @@ -569,6 +596,7 @@ mod proto { } } + #[cfg(not(target_os = "android"))] pub fn from_multi_clipbards(multi_clipboards: Vec) -> Vec { multi_clipboards .into_iter() @@ -597,3 +625,49 @@ mod proto { }) } } + +#[cfg(target_os = "android")] +pub fn handle_msg_clipboard(mut cb: Clipboard) { + use hbb_common::protobuf::Message; + + if cb.compress { + cb.content = bytes::Bytes::from(hbb_common::compress::decompress(&cb.content)); + } + let multi_clips = MultiClipboards { + clipboards: vec![cb], + ..Default::default() + }; + if let Ok(bytes) = multi_clips.write_to_bytes() { + let _ = scrap::android::ffi::call_clipboard_manager_update_clipboard(&bytes); + } +} + +#[cfg(target_os = "android")] +pub fn handle_msg_multi_clipboards(mut mcb: MultiClipboards) { + use hbb_common::protobuf::Message; + + for cb in mcb.clipboards.iter_mut() { + if cb.compress { + cb.content = bytes::Bytes::from(hbb_common::compress::decompress(&cb.content)); + } + } + if let Ok(bytes) = mcb.write_to_bytes() { + let _ = scrap::android::ffi::call_clipboard_manager_update_clipboard(&bytes); + } +} + +#[cfg(target_os = "android")] +pub fn get_clipboards_msg(client: bool) -> Option { + let mut clipboards = scrap::android::ffi::get_clipboards(client)?; + let mut msg = Message::new(); + for c in &mut clipboards.clipboards { + let compressed = hbb_common::compress::compress(&c.content); + let compress = compressed.len() < c.content.len(); + if compress { + c.content = compressed.into(); + } + c.compress = compress; + } + msg.set_multi_clipboards(clipboards); + Some(msg) +} diff --git a/src/flutter.rs b/src/flutter.rs index a1c9c7e3441c..f6fff4234813 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -1250,15 +1250,17 @@ fn try_send_close_event(event_stream: &Option>) { } } -#[cfg(not(any(target_os = "android", target_os = "ios")))] +#[cfg(not(target_os = "ios"))] pub fn update_text_clipboard_required() { let is_required = sessions::get_sessions() .iter() .any(|s| s.is_text_clipboard_required()); + #[cfg(target_os = "android")] + let _ = scrap::android::ffi::call_clipboard_manager_enable_client_clipboard(is_required); Client::set_is_text_clipboard_required(is_required); } -#[cfg(not(any(target_os = "android", target_os = "ios")))] +#[cfg(not(target_os = "ios"))] pub fn send_text_clipboard_msg(msg: Message) { for s in sessions::get_sessions() { if s.is_text_clipboard_required() { @@ -2051,7 +2053,7 @@ pub mod sessions { } #[inline] - #[cfg(not(any(target_os = "android", target_os = "ios")))] + #[cfg(not(target_os = "ios"))] pub fn has_sessions_running(conn_type: ConnType) -> bool { SESSIONS.read().unwrap().iter().any(|((_, r#type), s)| { *r#type == conn_type && s.session_handlers.read().unwrap().len() != 0 diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 7a0c5e87449d..ba03bc761300 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -274,7 +274,7 @@ pub fn session_toggle_option(session_id: SessionID, value: String) { session.toggle_option(value.clone()); try_sync_peer_option(&session, &session_id, &value, None); } - #[cfg(not(any(target_os = "android", target_os = "ios")))] + #[cfg(not(target_os = "ios"))] if sessions::get_session_by_session_id(&session_id).is_some() && value == "disable-clipboard" { crate::flutter::update_text_clipboard_required(); } @@ -817,6 +817,17 @@ pub fn main_show_option(_key: String) -> SyncReturn { SyncReturn(false) } +#[inline] +#[cfg(target_os = "android")] +fn enable_server_clipboard(keyboard_enabled: &str, clip_enabled: &str) { + use scrap::android::ffi::call_clipboard_manager_enable_service_clipboard; + let keyboard_enabled = + config::option2bool(config::keys::OPTION_ENABLE_KEYBOARD, &keyboard_enabled); + let clip_enabled = config::option2bool(config::keys::OPTION_ENABLE_CLIPBOARD, &clip_enabled); + crate::ui_cm_interface::switch_permission_all("clipboard".to_owned(), clip_enabled); + let _ = call_clipboard_manager_enable_service_clipboard(keyboard_enabled && clip_enabled); +} + pub fn main_set_option(key: String, value: String) { #[cfg(target_os = "android")] if key.eq(config::keys::OPTION_ENABLE_KEYBOARD) { @@ -824,6 +835,11 @@ pub fn main_set_option(key: String, value: String) { config::keys::OPTION_ENABLE_KEYBOARD, &value, )); + enable_server_clipboard(&value, &get_option(config::keys::OPTION_ENABLE_CLIPBOARD)); + } + #[cfg(target_os = "android")] + if key.eq(config::keys::OPTION_ENABLE_CLIPBOARD) { + enable_server_clipboard(&get_option(config::keys::OPTION_ENABLE_KEYBOARD), &value); } if key.eq("custom-rendezvous-server") { set_option(key, value.clone()); diff --git a/src/lang/ar.rs b/src/lang/ar.rs index 31fd680fd606..039ad4b114ec 100644 --- a/src/lang/ar.rs +++ b/src/lang/ar.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), + ("Update client clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/be.rs b/src/lang/be.rs index fbe16153543a..26281c26b4aa 100644 --- a/src/lang/be.rs +++ b/src/lang/be.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), + ("Update client clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/bg.rs b/src/lang/bg.rs index 4f0131cc878a..46126056c9be 100644 --- a/src/lang/bg.rs +++ b/src/lang/bg.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), + ("Update client clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ca.rs b/src/lang/ca.rs index 8e0ff1479cc7..d680b66a5e87 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), + ("Update client clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 12b3a4257e9a..d15c1b6ba0e1 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", "上传文件夹"), ("Upload files", "上传文件"), ("Clipboard is synchronized", "剪贴板已同步"), + ("Update client clipboard", "更新客户端的粘贴板"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index a9fb5b233cb0..c4ff80c7e3ff 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), + ("Update client clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index 34e5433f517f..905f31739480 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), + ("Update client clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index a732213712ab..b9e1e6cf9b59 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", "Ordner hochladen"), ("Upload files", "Dateien hochladen"), ("Clipboard is synchronized", "Zwischenablage ist synchronisiert"), + ("Update client clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/el.rs b/src/lang/el.rs index e6df3bc3d5b1..73a306c7c9aa 100644 --- a/src/lang/el.rs +++ b/src/lang/el.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), + ("Update client clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 876c901a481b..83747f03cd48 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), + ("Update client clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index ce77f620c717..7f3bf0f70f66 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", "Subir carpeta"), ("Upload files", "Subir archivos"), ("Clipboard is synchronized", "Portapapeles sincronizado"), + ("Update client clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/et.rs b/src/lang/et.rs index 21de56c9e69f..9f67c12262d7 100644 --- a/src/lang/et.rs +++ b/src/lang/et.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), + ("Update client clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eu.rs b/src/lang/eu.rs index ac958d79c67e..93f5a60b4ef6 100644 --- a/src/lang/eu.rs +++ b/src/lang/eu.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), + ("Update client clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index ff43815739a3..33c6b7427c1a 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), + ("Update client clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 85b1354c3184..8c8332ad1edc 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), + ("Update client clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/he.rs b/src/lang/he.rs index b63d42122469..400b5156b40e 100644 --- a/src/lang/he.rs +++ b/src/lang/he.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), + ("Update client clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hr.rs b/src/lang/hr.rs index 4a3136c833f6..d6389480a04d 100644 --- a/src/lang/hr.rs +++ b/src/lang/hr.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), + ("Update client clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index c80bb654da70..fc58fe5a6949 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", "Mappa feltöltése"), ("Upload files", "Fájlok feltöltése"), ("Clipboard is synchronized", "A vágólap szinkronizálva van"), + ("Update client clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index 066f2980cc18..b488f5740d9e 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), + ("Update client clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index e0dd83db6f7a..bee9d322bdc0 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", "Cartella upload"), ("Upload files", "File upload"), ("Clipboard is synchronized", "Gli appunti sono sincronizzati"), + ("Update client clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 5edd50572994..1d0f3b7ea157 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), + ("Update client clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 4960e752ab6b..f266f2536895 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", "폴더 업로드"), ("Upload files", "파일 업로드"), ("Clipboard is synchronized", "클립보드가 동기화됨"), + ("Update client clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index 07ca645f2728..46733ce713e5 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), + ("Update client clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lt.rs b/src/lang/lt.rs index 9a2069163ed4..723b46a30a81 100644 --- a/src/lang/lt.rs +++ b/src/lang/lt.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), + ("Update client clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lv.rs b/src/lang/lv.rs index 4c78dfbd9613..0439a45e65f5 100644 --- a/src/lang/lv.rs +++ b/src/lang/lv.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", "Augšupielādēt mapi"), ("Upload files", "Augšupielādēt failus"), ("Clipboard is synchronized", "Starpliktuve ir sinhronizēta"), + ("Update client clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nb.rs b/src/lang/nb.rs index a91e31e45bef..c9f3ce243921 100644 --- a/src/lang/nb.rs +++ b/src/lang/nb.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), + ("Update client clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nl.rs b/src/lang/nl.rs index ca46a32853e7..a4868391486c 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", "Map uploaden"), ("Upload files", "Bestanden uploaden"), ("Clipboard is synchronized", "Klembord is gesynchroniseerd"), + ("Update client clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index fd5641ac133e..fb77c9abb1a8 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", "Wyślij folder"), ("Upload files", "Wyślij pliki"), ("Clipboard is synchronized", "Schowek jest zsynchronizowany"), + ("Update client clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index c0564e0f4014..3fe7951870a4 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), + ("Update client clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index 14254388cc11..f382b7aba22d 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), + ("Update client clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ro.rs b/src/lang/ro.rs index cbce2f2a9278..7aaef0e01c4f 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), + ("Update client clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 6d173f1097b8..0344f7270dba 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", "Загрузить папку"), ("Upload files", "Загрузить файлы"), ("Clipboard is synchronized", "Буфер обмена синхронизирован"), + ("Update client clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index b3c8fddf9162..50ba1aeb080a 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), + ("Update client clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sl.rs b/src/lang/sl.rs index 20fd24c9ca0b..4e52bbe40fcb 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), + ("Update client clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sq.rs b/src/lang/sq.rs index 7c63c8ea5ab7..abab6acd8936 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), + ("Update client clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sr.rs b/src/lang/sr.rs index e80bb61812f4..96bf3e1e06da 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), + ("Update client clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sv.rs b/src/lang/sv.rs index dae48e7a368c..69806fa7f6f9 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), + ("Update client clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index 60b281851371..1b0cf69e4f25 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), + ("Update client clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/th.rs b/src/lang/th.rs index 71af446c144e..a657201e993b 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), + ("Update client clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index ce11544b5094..1b7b783d30f7 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), + ("Update client clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 86aad4fee2b8..fb9259ef9b67 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", "上傳資料夾"), ("Upload files", "上傳檔案"), ("Clipboard is synchronized", "剪貼簿已同步"), + ("Update client clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/uk.rs b/src/lang/uk.rs index ff5c8b64ae72..3ef8f4c6fdde 100644 --- a/src/lang/uk.rs +++ b/src/lang/uk.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), + ("Update client clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index 5a2c47befcc7..0d4751cd4f91 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -653,5 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), + ("Update client clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lib.rs b/src/lib.rs index 7f9ca4e9aafa..693f36dbcd65 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -45,7 +45,7 @@ mod custom_server; mod lang; #[cfg(not(any(target_os = "android", target_os = "ios")))] mod port_forward; -#[cfg(not(any(target_os = "android", target_os = "ios")))] +#[cfg(not(target_os = "ios"))] mod clipboard; #[cfg(all(feature = "flutter", feature = "plugin_framework"))] diff --git a/src/server.rs b/src/server.rs index 46c30b8bcf4b..ed2c9f2fd541 100644 --- a/src/server.rs +++ b/src/server.rs @@ -32,7 +32,7 @@ use crate::ipc::Data; pub mod audio_service; cfg_if::cfg_if! { -if #[cfg(not(any(target_os = "android", target_os = "ios")))] { +if #[cfg(not(target_os = "ios"))] { mod clipboard_service; #[cfg(target_os = "linux")] pub(crate) mod wayland; @@ -42,17 +42,20 @@ pub mod uinput; pub mod rdp_input; #[cfg(target_os = "linux")] pub mod dbus; +#[cfg(not(target_os = "android"))] pub mod input_service; } else { mod clipboard_service { pub const NAME: &'static str = ""; } -pub mod input_service { -pub const NAME_CURSOR: &'static str = ""; -pub const NAME_POS: &'static str = ""; -pub const NAME_WINDOW_FOCUS: &'static str = ""; } } + +#[cfg(any(target_os = "android", target_os = "ios"))] +pub mod input_service { + pub const NAME_CURSOR: &'static str = ""; + pub const NAME_POS: &'static str = ""; + pub const NAME_WINDOW_FOCUS: &'static str = ""; } mod connection; @@ -99,10 +102,12 @@ pub fn new() -> ServerPtr { }; server.add_service(Box::new(audio_service::new())); #[cfg(not(target_os = "ios"))] - server.add_service(Box::new(display_service::new())); - #[cfg(not(any(target_os = "android", target_os = "ios")))] { + server.add_service(Box::new(display_service::new())); server.add_service(Box::new(clipboard_service::new())); + } + #[cfg(not(any(target_os = "android", target_os = "ios")))] + { if !display_service::capture_cursor_embedded() { server.add_service(Box::new(input_service::new_cursor())); server.add_service(Box::new(input_service::new_pos())); diff --git a/src/server/clipboard_service.rs b/src/server/clipboard_service.rs index 3aadb3ad5dd2..401bb49336b4 100644 --- a/src/server/clipboard_service.rs +++ b/src/server/clipboard_service.rs @@ -1,11 +1,15 @@ use super::*; -pub use crate::clipboard::{ - check_clipboard, ClipboardContext, ClipboardSide, CLIPBOARD_INTERVAL as INTERVAL, - CLIPBOARD_NAME as NAME, -}; +#[cfg(not(target_os = "android"))] +pub use crate::clipboard::{check_clipboard, ClipboardContext, ClipboardSide}; +pub use crate::clipboard::{CLIPBOARD_INTERVAL as INTERVAL, CLIPBOARD_NAME as NAME}; #[cfg(windows)] use crate::ipc::{self, ClipboardFile, ClipboardNonFile, Data}; +#[cfg(not(target_os = "android"))] use clipboard_master::{CallbackResult, ClipboardHandler}; +#[cfg(target_os = "android")] +use hbb_common::config::{keys, option2bool}; +#[cfg(target_os = "android")] +use scrap::android::ffi::call_clipboard_manager_enable_service_clipboard; use std::{ io, sync::mpsc::{channel, RecvTimeoutError, Sender}, @@ -14,6 +18,7 @@ use std::{ #[cfg(windows)] use tokio::runtime::Runtime; +#[cfg(not(target_os = "android"))] struct Handler { sp: EmptyExtraFieldService, ctx: Option, @@ -25,11 +30,12 @@ struct Handler { } pub fn new() -> GenericService { - let svc = EmptyExtraFieldService::new(NAME.to_owned(), true); + let svc = EmptyExtraFieldService::new(NAME.to_owned(), false); GenericService::run(&svc.clone(), run); svc.sp } +#[cfg(not(target_os = "android"))] fn run(sp: EmptyExtraFieldService) -> ResultType<()> { let (tx_cb_result, rx_cb_result) = channel(); let handler = Handler { @@ -73,9 +79,9 @@ fn run(sp: EmptyExtraFieldService) -> ResultType<()> { Ok(()) } +#[cfg(not(target_os = "android"))] impl ClipboardHandler for Handler { fn on_clipboard_change(&mut self) -> CallbackResult { - self.sp.snapshot(|_sps| Ok(())).ok(); if self.sp.ok() { if let Some(msg) = self.get_clipboard_msg() { self.sp.send(msg); @@ -92,6 +98,7 @@ impl ClipboardHandler for Handler { } } +#[cfg(not(target_os = "android"))] impl Handler { fn get_clipboard_msg(&mut self) -> Option { #[cfg(target_os = "windows")] @@ -216,3 +223,25 @@ impl Handler { bail!("failed to get clipboard data from cm"); } } + +#[cfg(target_os = "android")] +fn is_clipboard_enabled() -> bool { + let keyboard_enabled = crate::ui_interface::get_option(keys::OPTION_ENABLE_KEYBOARD); + let keyboard_enabled = option2bool(keys::OPTION_ENABLE_KEYBOARD, &keyboard_enabled); + let clip_enabled = crate::ui_interface::get_option(keys::OPTION_ENABLE_CLIPBOARD); + let clip_enabled = option2bool(keys::OPTION_ENABLE_CLIPBOARD, &clip_enabled); + keyboard_enabled && clip_enabled +} + +#[cfg(target_os = "android")] +fn run(sp: EmptyExtraFieldService) -> ResultType<()> { + let _res = call_clipboard_manager_enable_service_clipboard(is_clipboard_enabled()); + while sp.ok() { + if let Some(msg) = crate::clipboard::get_clipboards_msg(false) { + sp.send(msg); + } + std::thread::sleep(Duration::from_millis(INTERVAL)); + } + let _res = call_clipboard_manager_enable_service_clipboard(false); + Ok(()) +} diff --git a/src/server/connection.rs b/src/server/connection.rs index 3315cac6972d..4bdda795f0b3 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -690,7 +690,7 @@ impl Connection { } } Some(message::Union::MultiClipboards(_multi_clipboards)) => { - #[cfg(not(any(target_os = "android", target_os = "ios")))] + #[cfg(not(target_os = "ios"))] if let Some(msg_out) = crate::clipboard::get_msg_if_not_support_multi_clip(&conn.lr.version, &conn.lr.my_platform, _multi_clipboards) { if let Err(err) = conn.stream.send(&msg_out).await { conn.on_close(&err.to_string(), false).await; @@ -2074,7 +2074,9 @@ impl Connection { if self.clipboard { #[cfg(not(any(target_os = "android", target_os = "ios")))] update_clipboard(vec![cb], ClipboardSide::Host); - #[cfg(all(feature = "flutter", target_os = "android"))] + // ios as the controlled side is actually not supported for now. + // The following code is only used to preserve the logic of handling text clipboard on mobile. + #[cfg(target_os = "ios")] { let content = if cb.compress { hbb_common::compress::decompress(&cb.content) @@ -2092,14 +2094,17 @@ impl Connection { } } } + #[cfg(target_os = "android")] + crate::clipboard::handle_msg_clipboard(cb); } } - Some(message::Union::MultiClipboards(_mcb)) => - { + Some(message::Union::MultiClipboards(_mcb)) => { #[cfg(not(any(target_os = "android", target_os = "ios")))] if self.clipboard { update_clipboard(_mcb.clipboards, ClipboardSide::Host); } + #[cfg(target_os = "android")] + crate::clipboard::handle_msg_multi_clipboards(_mcb); } Some(message::Union::Cliprdr(_clip)) => { diff --git a/src/ui_cm_interface.rs b/src/ui_cm_interface.rs index c34e15e26c84..c34671d57a56 100644 --- a/src/ui_cm_interface.rs +++ b/src/ui_cm_interface.rs @@ -312,6 +312,17 @@ pub fn switch_permission(id: i32, name: String, enabled: bool) { }; } +#[inline] +#[cfg(target_os = "android")] +pub fn switch_permission_all(name: String, enabled: bool) { + for (_, client) in CLIENTS.read().unwrap().iter() { + allow_err!(client.tx.send(Data::SwitchPermission { + name: name.clone(), + enabled + })); + } +} + #[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))] #[inline] pub fn get_clients_state() -> String { diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 321707d3f63e..4bd4b6e2d148 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -354,7 +354,7 @@ impl Session { self.lc.read().unwrap().is_privacy_mode_supported() } - #[cfg(not(any(target_os = "android", target_os = "ios")))] + #[cfg(not(target_os = "ios"))] pub fn is_text_clipboard_required(&self) -> bool { *self.server_clipboard_enabled.read().unwrap() && *self.server_keyboard_enabled.read().unwrap() @@ -526,10 +526,7 @@ impl Session { #[cfg(not(feature = "flutter"))] #[cfg(not(any(target_os = "android", target_os = "ios")))] pub fn is_xfce(&self) -> bool { - #[cfg(not(any(target_os = "ios")))] - return crate::platform::is_xfce(); - #[cfg(any(target_os = "ios"))] - false + crate::platform::is_xfce() } pub fn remove_port_forward(&self, port: i32) { From c5426b0fbc60dbca90c68d0e32090ca49055e6ab Mon Sep 17 00:00:00 2001 From: 21pages Date: Mon, 18 Nov 2024 23:03:27 +0800 Subject: [PATCH 387/541] Fix hevc decode error "Could not find ref with POC" (#9960) 1. Dropping frames can cause this error, reset encoder when this happens. 2. There are some logic error for clear video queue, because video queue message is not cleared. This need to be fixed. Signed-off-by: 21pages --- Cargo.lock | 4 ++-- libs/scrap/src/common/hwcodec.rs | 3 ++- src/client.rs | 18 ++++++++++++++++++ src/client/io_loop.rs | 8 ++++++-- 4 files changed, 28 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5930b87216d4..b8a427f7207d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1683,7 +1683,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" dependencies = [ - "libloading 0.8.4", + "libloading 0.7.4", ] [[package]] @@ -3051,7 +3051,7 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hwcodec" version = "0.7.0" -source = "git+https://github.com/rustdesk-org/hwcodec#8bbd05bb300ad07cc345356ad85570f9ea99fbfa" +source = "git+https://github.com/rustdesk-org/hwcodec#da7dab48df19edb5a7138ff9e01bf9f148b523da" dependencies = [ "bindgen 0.59.2", "cc", diff --git a/libs/scrap/src/common/hwcodec.rs b/libs/scrap/src/common/hwcodec.rs index a0e730c91db6..2a83afacc8e4 100644 --- a/libs/scrap/src/common/hwcodec.rs +++ b/libs/scrap/src/common/hwcodec.rs @@ -15,7 +15,7 @@ use hbb_common::{ }; use hwcodec::{ common::{ - DataFormat, + DataFormat, HwcodecErrno, Quality::{self, *}, RateControl::{self, *}, }, @@ -31,6 +31,7 @@ const DEFAULT_PIXFMT: AVPixelFormat = AVPixelFormat::AV_PIX_FMT_NV12; pub const DEFAULT_FPS: i32 = 30; const DEFAULT_GOP: i32 = i32::MAX; const DEFAULT_HW_QUALITY: Quality = Quality_Default; +pub const ERR_HEVC_POC: i32 = HwcodecErrno::HWCODEC_ERR_HEVC_COULD_NOT_FIND_POC as i32; crate::generate_call_macro!(call_yuv, false); diff --git a/src/client.rs b/src/client.rs index c36063530fce..18c3d268bb1c 100644 --- a/src/client.rs +++ b/src/client.rs @@ -2417,6 +2417,24 @@ where // to-do: fix the error log::error!("handle video frame error, {}", e); session.refresh_video(display as _); + #[cfg(feature = "hwcodec")] + if format == CodecFormat::H265 { + if let Some(&scrap::hwcodec::ERR_HEVC_POC) = + e.downcast_ref::() + { + for (i, handler_controler) in + handler_controller_map.iter_mut() + { + if *i != display + && handler_controler.handler.decoder.format() + == CodecFormat::H265 + { + log::info!("refresh video {} due to hevc poc not found", i); + session.refresh_video(*i as _); + } + } + } + } } _ => {} } diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index 9aec89dd3230..0766b6b51bdd 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -1146,9 +1146,13 @@ impl Remote { .ok(); } else { if let Some(video_queue) = video_queue_write.get_mut(&display) { - video_queue.force_push(vf); + if video_queue.force_push(vf).is_some() { + while let Some(_) = video_queue.pop() {} + self.handler.refresh_video(display as _); + } else { + self.video_sender.send(MediaData::VideoQueue(display)).ok(); + } } - self.video_sender.send(MediaData::VideoQueue(display)).ok(); } self.fps_control .last_active_time From b990ff3782bb92f30b9b6a1cac6d43299c6bb122 Mon Sep 17 00:00:00 2001 From: bovirus <1262554+bovirus@users.noreply.github.com> Date: Tue, 19 Nov 2024 01:15:37 +0100 Subject: [PATCH 388/541] Update Italian language (#9961) --- src/lang/it.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lang/it.rs b/src/lang/it.rs index bee9d322bdc0..b5a191f093ef 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -653,6 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", "Cartella upload"), ("Upload files", "File upload"), ("Clipboard is synchronized", "Gli appunti sono sincronizzati"), - ("Update client clipboard", ""), + ("Update client clipboard", "Aggiorna appunti client"), ].iter().cloned().collect(); } From 251e1a3487518f763e0bb2b7eafb505fbd75b56d Mon Sep 17 00:00:00 2001 From: solokot Date: Tue, 19 Nov 2024 03:15:50 +0300 Subject: [PATCH 389/541] Update ru.rs (#9962) --- src/lang/ru.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 0344f7270dba..bcc5ed996740 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -653,6 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", "Загрузить папку"), ("Upload files", "Загрузить файлы"), ("Clipboard is synchronized", "Буфер обмена синхронизирован"), - ("Update client clipboard", ""), + ("Update client clipboard", "Обновить буфер обмена клиента"), ].iter().cloned().collect(); } From f760e21ff8ab442579a4d8487b9138fe8dbfe6ec Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Tue, 19 Nov 2024 15:03:00 +0800 Subject: [PATCH 390/541] fix: android w&h, refresh when no connection (#9966) Signed-off-by: fufesou --- .../app/src/main/kotlin/com/carriez/flutter_hbb/MainService.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainService.kt b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainService.kt index 1a709747e25c..4c40e3349ed8 100644 --- a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainService.kt +++ b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainService.kt @@ -302,6 +302,8 @@ class MainService : Service() { stopCapture() FFI.refreshScreen() startCapture() + } else { + FFI.refreshScreen() } } From 4a49fbe4a62977f6d121233fb0b4bf7698601476 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Tue, 19 Nov 2024 17:29:28 +0800 Subject: [PATCH 391/541] fix: privacy mode 2 (#9972) Do not change the resolutions when setting the new primary display. Signed-off-by: fufesou --- src/privacy_mode/win_virtual_display.rs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/privacy_mode/win_virtual_display.rs b/src/privacy_mode/win_virtual_display.rs index 782d7ed75a87..d235575fdacf 100644 --- a/src/privacy_mode/win_virtual_display.rs +++ b/src/privacy_mode/win_virtual_display.rs @@ -171,8 +171,9 @@ impl PrivacyModeImpl { } } - fn set_primary_display(&mut self) -> ResultType<()> { + fn set_primary_display(&mut self) -> ResultType { let display = &self.virtual_displays[0]; + let display_name = std::string::String::from_utf16(&display.name)?; #[allow(invalid_value)] let mut new_primary_dm: DEVMODEW = unsafe { std::mem::MaybeUninit::uninit().assume_init() }; @@ -230,8 +231,6 @@ impl PrivacyModeImpl { dm.u1.s2_mut().dmPosition.x -= new_primary_dm.u1.s2().dmPosition.x; dm.u1.s2_mut().dmPosition.y -= new_primary_dm.u1.s2().dmPosition.y; dm.dmFields |= DM_POSITION; - dm.dmPelsWidth = 1920; - dm.dmPelsHeight = 1080; let rc = ChangeDisplaySettingsExW( dd.DeviceName.as_ptr(), &mut dm, @@ -261,7 +260,7 @@ impl PrivacyModeImpl { } } - Ok(()) + Ok(display_name) } fn disable_physical_displays(&self) -> ResultType<()> { @@ -431,9 +430,11 @@ impl PrivacyMode for PrivacyModeImpl { } let reg_connectivity_1 = reg_display_settings::read_reg_connectivity()?; - guard.set_primary_display()?; + let primary_display_name = guard.set_primary_display()?; guard.disable_physical_displays()?; Self::commit_change_display(CDS_RESET)?; + // Explicitly set the resolution(virtual display) to 1920x1080. + allow_err!(crate::platform::change_resolution(&primary_display_name, 1920, 1080)); let reg_connectivity_2 = reg_display_settings::read_reg_connectivity()?; if let Some(reg_recovery) = From 608d7d55d5c85c96f2846a20d2d090cd39fd2f88 Mon Sep 17 00:00:00 2001 From: KAYUII <577738@qq.com> Date: Wed, 20 Nov 2024 19:41:30 +0800 Subject: [PATCH 392/541] add env VCPKG_INSTALLED_ROOT (#9985) --- build.rs | 6 +++++- libs/scrap/build.rs | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/build.rs b/build.rs index 7333fff7b4b1..3d19ee037f6f 100644 --- a/build.rs +++ b/build.rs @@ -61,7 +61,11 @@ fn install_android_deps() { let target = format!("{}-android", target_arch); let vcpkg_root = std::env::var("VCPKG_ROOT").unwrap(); let mut path: std::path::PathBuf = vcpkg_root.into(); - path.push("installed"); + if let Ok(vcpkg_root) = std::env::var("VCPKG_INSTALLED_ROOT") { + path = vcpkg_root.into(); + } else { + path.push("installed"); + } path.push(target); println!( "{}", diff --git a/libs/scrap/build.rs b/libs/scrap/build.rs index 55a688633817..0c0cd274ea7b 100644 --- a/libs/scrap/build.rs +++ b/libs/scrap/build.rs @@ -55,7 +55,11 @@ fn link_vcpkg(mut path: PathBuf, name: &str) -> PathBuf { target = target.replace("x64", "x86"); } println!("cargo:info={}", target); - path.push("installed"); + if let Ok(vcpkg_root) = std::env::var("VCPKG_INSTALLED_ROOT") { + path = vcpkg_root.into(); + } else { + path.push("installed"); + } path.push(target); println!( "{}", From d4cb7d68c5506dd47936da8d06d19d2f3ef0d52a Mon Sep 17 00:00:00 2001 From: Mr-Update <37781396+Mr-Update@users.noreply.github.com> Date: Wed, 20 Nov 2024 12:41:43 +0100 Subject: [PATCH 393/541] Update de.rs (#9975) --- src/lang/de.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lang/de.rs b/src/lang/de.rs index b9e1e6cf9b59..ba91471c92d4 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -364,7 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recording", "Aufnahme"), ("Directory", "Verzeichnis"), ("Automatically record incoming sessions", "Eingehende Sitzungen automatisch aufzeichnen"), - ("Automatically record outgoing sessions", ""), + ("Automatically record outgoing sessions", "Ausgehende Sitzungen automatisch aufzeichnen"), ("Change", "Ändern"), ("Start session recording", "Sitzungsaufzeichnung starten"), ("Stop session recording", "Sitzungsaufzeichnung beenden"), @@ -653,6 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", "Ordner hochladen"), ("Upload files", "Dateien hochladen"), ("Clipboard is synchronized", "Zwischenablage ist synchronisiert"), - ("Update client clipboard", ""), + ("Update client clipboard", "Client-Zwischenablage aktualisieren"), ].iter().cloned().collect(); } From bc211c8031ac3b9b39ab14820ac1d8d064c84580 Mon Sep 17 00:00:00 2001 From: 21pages Date: Wed, 20 Nov 2024 19:44:24 +0800 Subject: [PATCH 394/541] A=b, A case insensitive (#9976) Signed-off-by: 21pages --- flutter/lib/common.dart | 12 ++++++++---- flutter/pubspec.lock | 8 ++++---- flutter/pubspec.yaml | 2 +- src/client.rs | 10 +++++----- src/custom_server.rs | 25 +++++++++++++++++++------ 5 files changed, 37 insertions(+), 20 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 71e7f68d32cd..fb389b45e646 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -2245,7 +2245,10 @@ List? urlLinkToCmdArgs(Uri uri) { } } - var key = uri.queryParameters["key"]; + var queryParameters = + uri.queryParameters.map((k, v) => MapEntry(k.toLowerCase(), v)); + + var key = queryParameters["key"]; if (id != null) { if (key != null) { id = "$id?key=$key"; @@ -2254,7 +2257,7 @@ List? urlLinkToCmdArgs(Uri uri) { if (isMobile) { if (id != null) { - final forceRelay = uri.queryParameters["relay"] != null; + final forceRelay = queryParameters["relay"] != null; connect(Get.context!, id, forceRelay: forceRelay); return null; } @@ -2264,7 +2267,7 @@ List? urlLinkToCmdArgs(Uri uri) { if (command != null && id != null) { args.add(command); args.add(id); - var param = uri.queryParameters; + var param = queryParameters; String? password = param["password"]; if (password != null) args.addAll(['--password', password]); String? switch_uuid = param["switch_uuid"]; @@ -2510,7 +2513,8 @@ Future onActiveWindowChanged() async { // embedder.cc (2672): 'FlutterEngineSendPlatformMessage' returned 'kInvalidArguments'. Invalid engine handle. // 2024-11-11 11:41:11.565 RustDesk[90272:2567686] Failed to send message to Flutter engine on channel 'flutter/lifecycle' (2). // ``` - periodic_immediate(Duration(milliseconds: 30), RdPlatformChannel.instance.terminate); + periodic_immediate( + Duration(milliseconds: 30), RdPlatformChannel.instance.terminate); } } } diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index 4010edf974d1..7c60e037a57d 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -745,10 +745,10 @@ packages: dependency: "direct main" description: name: image_picker - sha256: b6951e25b795d053a6ba03af5f710069c99349de9341af95155d52665cb4607c + sha256: "021834d9c0c3de46bf0fe40341fa07168407f694d9b2bb18d532dc1261867f7a" url: "https://pub.dev" source: hosted - version: "0.8.9" + version: "1.1.2" image_picker_android: dependency: transitive description: @@ -793,10 +793,10 @@ packages: dependency: transitive description: name: image_picker_platform_interface - sha256: fa4e815e6fcada50e35718727d83ba1c92f1edf95c0b4436554cec301b56233b + sha256: "9ec26d410ff46f483c5519c29c02ef0e02e13a543f882b152d4bfd2f06802f80" url: "https://pub.dev" source: hosted - version: "2.9.3" + version: "2.10.0" image_picker_windows: dependency: transitive description: diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index 1855aebec007..afe09a0dc72e 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -46,7 +46,7 @@ dependencies: http: ^1.1.0 qr_code_scanner: ^1.0.0 zxing2: ^0.2.0 - image_picker: ^0.8.5 + image_picker: ^1.1.2 image: ^4.0.17 back_button_interceptor: ^6.0.1 flutter_rust_bridge: "1.80.1" diff --git a/src/client.rs b/src/client.rs index 18c3d268bb1c..503870cc182f 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1408,18 +1408,18 @@ impl LoginConfigHandler { let server = server_key.next().unwrap_or_default(); let args = server_key.next().unwrap_or_default(); let key = if server == PUBLIC_SERVER { - PUBLIC_RS_PUB_KEY + PUBLIC_RS_PUB_KEY.to_owned() } else { - let mut args_map: HashMap<&str, &str> = HashMap::new(); + let mut args_map: HashMap = HashMap::new(); for arg in args.split('&') { if let Some(kv) = arg.find('=') { - let k = &arg[0..kv]; + let k = arg[0..kv].to_lowercase(); let v = &arg[kv + 1..]; args_map.insert(k, v); } } let key = args_map.remove("key").unwrap_or_default(); - key + key.to_owned() }; // here we can check /r@server @@ -1427,7 +1427,7 @@ impl LoginConfigHandler { if real_id != raw_id { force_relay = true; } - self.other_server = Some((real_id.clone(), server.to_owned(), key.to_owned())); + self.other_server = Some((real_id.clone(), server.to_owned(), key)); id = format!("{real_id}@{server}"); } else { let real_id = crate::ui_interface::handle_relay_id(&id); diff --git a/src/custom_server.rs b/src/custom_server.rs index c2c5e7f63042..18118788e323 100644 --- a/src/custom_server.rs +++ b/src/custom_server.rs @@ -56,8 +56,8 @@ pub fn get_custom_server_from_string(s: &str) -> ResultType { * * This allows using a ',' (comma) symbol as a final delimiter. */ - if s.contains("host=") { - let stripped = &s[s.find("host=").unwrap_or(0)..s.len()]; + if s.to_lowercase().contains("host=") { + let stripped = &s[s.to_lowercase().find("host=").unwrap_or(0)..s.len()]; let strs: Vec<&str> = stripped.split(",").collect(); let mut host = String::default(); let mut key = String::default(); @@ -65,16 +65,17 @@ pub fn get_custom_server_from_string(s: &str) -> ResultType { let mut relay = String::default(); let strs_iter = strs.iter(); for el in strs_iter { - if el.starts_with("host=") { + let el_lower = el.to_lowercase(); + if el_lower.starts_with("host=") { host = el.chars().skip(5).collect(); } - if el.starts_with("key=") { + if el_lower.starts_with("key=") { key = el.chars().skip(4).collect(); } - if el.starts_with("api=") { + if el_lower.starts_with("api=") { api = el.chars().skip(4).collect(); } - if el.starts_with("relay=") { + if el_lower.starts_with("relay=") { relay = el.chars().skip(6).collect(); } } @@ -169,6 +170,18 @@ mod test { relay: "server.example.net".to_owned(), } ); + assert_eq!( + get_custom_server_from_string( + "rustdesk-Host=server.example.net,Key=Zm9vYmFyLiwyCg==,RELAY=server.example.net.exe" + ) + .unwrap(), + CustomServer { + host: "server.example.net".to_owned(), + key: "Zm9vYmFyLiwyCg==".to_owned(), + api: "".to_owned(), + relay: "server.example.net".to_owned(), + } + ); let lic = CustomServer { host: "1.1.1.1".to_owned(), key: "5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=".to_owned(), From d26fea41ee40c19a67ef9a7d59e3c270dee60386 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Thu, 21 Nov 2024 02:26:51 +0900 Subject: [PATCH 395/541] upgrade The-Fat-Controller --- Cargo.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index b8a427f7207d..9b450437d97e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6445,7 +6445,7 @@ dependencies = [ [[package]] name = "tfc" version = "0.7.0" -source = "git+https://github.com/rustdesk-org/The-Fat-Controller?branch=history/rebase_upstream_20240722#de9c8ba480f166a9fc90aaa47bb0e84b443ea9c6" +source = "git+https://github.com/rustdesk-org/The-Fat-Controller?branch=history/rebase_upstream_20240722#78bb80a8e596e4c14ae57c8448f5fca75f91f2b0" dependencies = [ "anyhow", "core-graphics 0.23.2", From 74dd0c8fa0c8e6754e54d9e5637900c1ca89ac19 Mon Sep 17 00:00:00 2001 From: zyl Date: Thu, 21 Nov 2024 13:36:11 +0800 Subject: [PATCH 396/541] fix mis-align problem when converting &[u8] to &[f32] (#9986) * fix: windows, improve audio buffer (#9770) * . * fix statics does not record and avoid channel changing when drio audio when audio is stero * add some commence * fix mis-align problem when converting &[u8] to &[f32] * add safety commence * revert client.rs * avoid tmp lifetime extends * avoid move in loop * avoid use after drop * another use after free * another use after free * make code more reasonable --------- Co-authored-by: zylthinking --- libs/hbb_common/src/lib.rs | 1 + libs/hbb_common/src/mem.rs | 14 ++++++++++++++ src/server/audio_service.rs | 19 +++++++++++++++++++ 3 files changed, 34 insertions(+) create mode 100644 libs/hbb_common/src/mem.rs diff --git a/libs/hbb_common/src/lib.rs b/libs/hbb_common/src/lib.rs index 15ef31022c42..36a68550fa52 100644 --- a/libs/hbb_common/src/lib.rs +++ b/libs/hbb_common/src/lib.rs @@ -27,6 +27,7 @@ pub use anyhow::{self, bail}; pub use futures_util; pub mod config; pub mod fs; +pub mod mem; pub use lazy_static; #[cfg(not(any(target_os = "android", target_os = "ios")))] pub use mac_address; diff --git a/libs/hbb_common/src/mem.rs b/libs/hbb_common/src/mem.rs new file mode 100644 index 000000000000..90a5d6d402ed --- /dev/null +++ b/libs/hbb_common/src/mem.rs @@ -0,0 +1,14 @@ +/// SAFETY: the returned Vec must not be resized or reserverd +pub unsafe fn aligned_u8_vec(cap: usize, align: usize) -> Vec { + use std::alloc::*; + + let layout = + Layout::from_size_align(cap, align).expect("invalid aligned value, must be power of 2"); + unsafe { + let ptr = alloc(layout); + if ptr.is_null() { + panic!("failed to allocate {} bytes", cap); + } + Vec::from_raw_parts(ptr, 0, cap) + } +} diff --git a/src/server/audio_service.rs b/src/server/audio_service.rs index f227bd232880..5d60abf6cb81 100644 --- a/src/server/audio_service.rs +++ b/src/server/audio_service.rs @@ -78,6 +78,19 @@ pub fn restart() { #[cfg(any(target_os = "linux", target_os = "android"))] mod pa_impl { use super::*; + + // SAFETY: constrains of hbb_common::mem::aligned_u8_vec must be held + unsafe fn align_to_32(data: Vec) -> Vec { + if (data.as_ptr() as usize & 3) == 0 { + return data; + } + + let mut buf = vec![]; + buf = unsafe { hbb_common::mem::aligned_u8_vec(data.len(), 4) }; + buf.extend_from_slice(data.as_ref()); + buf + } + #[tokio::main(flavor = "current_thread")] pub async fn run(sp: EmptyExtraFieldService) -> ResultType<()> { hbb_common::sleep(0.1).await; // one moment to wait for _pa ipc @@ -106,23 +119,29 @@ mod pa_impl { sps.send(create_format_msg(crate::platform::PA_SAMPLE_RATE, 2)); Ok(()) })?; + #[cfg(target_os = "linux")] if let Ok(data) = stream.next_raw().await { if data.len() == 0 { send_f32(&zero_audio_frame, &mut encoder, &sp); continue; } + if data.len() != AUDIO_DATA_SIZE_U8 { continue; } + + let data = unsafe { align_to_32(data.into()) }; let data = unsafe { std::slice::from_raw_parts::(data.as_ptr() as _, data.len() / 4) }; send_f32(data, &mut encoder, &sp); } + #[cfg(target_os = "android")] if scrap::android::ffi::get_audio_raw(&mut android_data, &mut vec![]).is_some() { let data = unsafe { + android_data = align_to_32(android_data); std::slice::from_raw_parts::( android_data.as_ptr() as _, android_data.len() / 4, From 1c99eb55008e21dd041a6b756d631d1243657753 Mon Sep 17 00:00:00 2001 From: Alex Rijckaert Date: Thu, 21 Nov 2024 12:12:24 +0100 Subject: [PATCH 397/541] Update nl.rs (#9997) --- src/lang/nl.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lang/nl.rs b/src/lang/nl.rs index a4868391486c..78c6f77b753a 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -653,6 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", "Map uploaden"), ("Upload files", "Bestanden uploaden"), ("Clipboard is synchronized", "Klembord is gesynchroniseerd"), - ("Update client clipboard", ""), + ("Update client clipboard", "Klembord van client bijwerken"), ].iter().cloned().collect(); } From 64654ee7cfa35c274ebf41d6b776c16f9ada46d5 Mon Sep 17 00:00:00 2001 From: 21pages Date: Fri, 22 Nov 2024 00:02:25 +0800 Subject: [PATCH 398/541] seperate video decoding thread for each display (#9968) * seperate video decoding thread for each display 1. Separate Video Decoding Thread for Each Display 2. Fix Decode Errors When Clearing the Queue Previously, on-flight frames after clearing the queue could not be decoded successfully. This issue can be resolved by setting a discard_queue flag when sending a refresh message. The flag will be reset upon receiving a keyframe. Signed-off-by: 21pages * update video format along with fps to flutter Signed-off-by: 21pages * Fix keyframe interval when auto record outgoing sessions Signed-off-by: 21pages --------- Signed-off-by: 21pages --- src/client.rs | 173 ++++++------------- src/client/io_loop.rs | 324 ++++++++++++++++++++++-------------- src/server/video_service.rs | 16 +- src/ui_session_interface.rs | 39 +---- 4 files changed, 253 insertions(+), 299 deletions(-) diff --git a/src/client.rs b/src/client.rs index 503870cc182f..97fa3a0424a2 100644 --- a/src/client.rs +++ b/src/client.rs @@ -908,6 +908,7 @@ impl AudioBuffer { } self.2[i] += 1; + #[allow(non_upper_case_globals)] static mut tms: i64 = 0; let dt = Local::now().timestamp_millis(); unsafe { @@ -2274,74 +2275,60 @@ impl LoginConfigHandler { /// Media data. pub enum MediaData { - VideoQueue(usize), + VideoQueue, VideoFrame(Box), AudioFrame(Box), AudioFormat(AudioFormat), - Reset(Option), + Reset, RecordScreen(bool), } pub type MediaSender = mpsc::Sender; -struct VideoHandlerController { - handler: VideoHandler, - skip_beginning: u32, -} - -/// Start video and audio thread. -/// Return two [`MediaSender`], they should be given to the media producer. +/// Start video thread. /// /// # Arguments /// /// * `video_callback` - The callback for video frame. Being called when a video frame is ready. -pub fn start_video_audio_threads( +pub fn start_video_thread( session: Session, + display: usize, + video_receiver: mpsc::Receiver, + video_queue: Arc>>, + fps: Arc>>, + chroma: Arc>>, + discard_queue: Arc>, video_callback: F, -) -> ( - MediaSender, - MediaSender, - Arc>>>, - Arc>>, - Arc>>, -) -where +) where F: 'static + FnMut(usize, &mut scrap::ImageRgb, *mut c_void, bool) + Send, T: InvokeUiSession, { - let (video_sender, video_receiver) = mpsc::channel::(); - let video_queue_map: Arc>>> = Default::default(); - let video_queue_map_cloned = video_queue_map.clone(); let mut video_callback = video_callback; - - let fps = Arc::new(RwLock::new(None)); - let decode_fps_map = fps.clone(); - let chroma = Arc::new(RwLock::new(None)); - let chroma_cloned = chroma.clone(); let mut last_chroma = None; std::thread::spawn(move || { #[cfg(windows)] sync_cpu_usage(); get_hwcodec_config(); - let mut handler_controller_map = HashMap::new(); + let mut video_handler = None; let mut count = 0; let mut duration = std::time::Duration::ZERO; + let mut skip_beginning = 0; loop { if let Ok(data) = video_receiver.recv() { match data { - MediaData::VideoFrame(_) | MediaData::VideoQueue(_) => { + MediaData::VideoFrame(_) | MediaData::VideoQueue => { let vf = match data { - MediaData::VideoFrame(vf) => *vf, - MediaData::VideoQueue(display) => { - if let Some(video_queue) = - video_queue_map.read().unwrap().get(&display) - { - if let Some(vf) = video_queue.pop() { - vf - } else { + MediaData::VideoFrame(vf) => { + *discard_queue.write().unwrap() = false; + *vf + } + MediaData::VideoQueue => { + if let Some(vf) = video_queue.read().unwrap().pop() { + if discard_queue.read().unwrap().clone() { continue; } + vf } else { continue; } @@ -2354,36 +2341,25 @@ where let display = vf.display as usize; let start = std::time::Instant::now(); let format = CodecFormat::from(&vf); - if !handler_controller_map.contains_key(&display) { + if video_handler.is_none() { let mut handler = VideoHandler::new(format, display); let record = session.lc.read().unwrap().record; let id = session.lc.read().unwrap().id.clone(); if record { handler.record_screen(record, id, display); } - handler_controller_map.insert( - display, - VideoHandlerController { - handler, - skip_beginning: 0, - }, - ); + video_handler = Some(handler); } - if let Some(handler_controller) = handler_controller_map.get_mut(&display) { + if let Some(handler) = video_handler.as_mut() { let mut pixelbuffer = true; let mut tmp_chroma = None; - let format_changed = - handler_controller.handler.decoder.format() != format; - match handler_controller.handler.handle_frame( - vf, - &mut pixelbuffer, - &mut tmp_chroma, - ) { + let format_changed = handler.decoder.format() != format; + match handler.handle_frame(vf, &mut pixelbuffer, &mut tmp_chroma) { Ok(true) => { video_callback( display, - &mut handler_controller.handler.rgb, - handler_controller.handler.texture.texture, + &mut handler.rgb, + handler.texture.texture, pixelbuffer, ); @@ -2395,7 +2371,7 @@ where // fps calculation fps_calculate( - handler_controller, + &mut skip_beginning, &fps, format_changed, start.elapsed(), @@ -2417,24 +2393,6 @@ where // to-do: fix the error log::error!("handle video frame error, {}", e); session.refresh_video(display as _); - #[cfg(feature = "hwcodec")] - if format == CodecFormat::H265 { - if let Some(&scrap::hwcodec::ERR_HEVC_POC) = - e.downcast_ref::() - { - for (i, handler_controler) in - handler_controller_map.iter_mut() - { - if *i != display - && handler_controler.handler.decoder.format() - == CodecFormat::H265 - { - log::info!("refresh video {} due to hevc poc not found", i); - session.refresh_video(*i as _); - } - } - } - } } _ => {} } @@ -2442,53 +2400,36 @@ where // check invalid decoders let mut should_update_supported = false; - handler_controller_map - .iter() - .map(|(_, h)| { - if !h.handler.decoder.valid() || h.handler.fail_counter >= MAX_DECODE_FAIL_COUNTER { - let mut lc = session.lc.write().unwrap(); - let format = h.handler.decoder.format(); - if !lc.mark_unsupported.contains(&format) { - lc.mark_unsupported.push(format); - should_update_supported = true; - log::info!("mark {format:?} decoder as unsupported, valid:{}, fail_counter:{}, all unsupported:{:?}", h.handler.decoder.valid(), h.handler.fail_counter, lc.mark_unsupported); - } + if let Some(handler) = video_handler.as_mut() { + if !handler.decoder.valid() + || handler.fail_counter >= MAX_DECODE_FAIL_COUNTER + { + let mut lc = session.lc.write().unwrap(); + let format = handler.decoder.format(); + if !lc.mark_unsupported.contains(&format) { + lc.mark_unsupported.push(format); + should_update_supported = true; + log::info!("mark {format:?} decoder as unsupported, valid:{}, fail_counter:{}, all unsupported:{:?}", handler.decoder.valid(), handler.fail_counter, lc.mark_unsupported); } - }) - .count(); + } + } if should_update_supported { session.send(Data::Message( session.lc.read().unwrap().update_supported_decodings(), )); } } - MediaData::Reset(display) => { - if let Some(display) = display { - if let Some(handler_controler) = - handler_controller_map.get_mut(&display) - { - handler_controler.handler.reset(None); - } - } else { - for (_, handler_controler) in handler_controller_map.iter_mut() { - handler_controler.handler.reset(None); - } + MediaData::Reset => { + if let Some(handler) = video_handler.as_mut() { + handler.reset(None); } } MediaData::RecordScreen(start) => { log::info!("record screen command: start: {start}"); - let record = session.lc.read().unwrap().record; session.update_record_status(start); - if record != start { - session.lc.write().unwrap().record = start; - let id = session.lc.read().unwrap().id.clone(); - for (display, handler_controler) in handler_controller_map.iter_mut() { - handler_controler.handler.record_screen( - start, - id.clone(), - *display, - ); - } + let id = session.lc.read().unwrap().id.clone(); + if let Some(handler) = video_handler.as_mut() { + handler.record_screen(start, id, display); } } _ => {} @@ -2499,14 +2440,6 @@ where } log::info!("Video decoder loop exits"); }); - let audio_sender = start_audio_thread(); - return ( - video_sender, - audio_sender, - video_queue_map_cloned, - decode_fps_map, - chroma_cloned, - ); } /// Start an audio thread @@ -2538,7 +2471,7 @@ pub fn start_audio_thread() -> MediaSender { #[inline] fn fps_calculate( - handler_controller: &mut VideoHandlerController, + skip_beginning: &mut usize, fps: &Arc>>, format_changed: bool, elapsed: std::time::Duration, @@ -2548,11 +2481,11 @@ fn fps_calculate( if format_changed { *count = 0; *duration = std::time::Duration::ZERO; - handler_controller.skip_beginning = 0; + *skip_beginning = 0; } // // The first frame will be very slow - if handler_controller.skip_beginning < 3 { - handler_controller.skip_beginning += 1; + if *skip_beginning < 3 { + *skip_beginning += 1; return; } *duration += elapsed; diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index 0766b6b51bdd..8dfdffcfe1c6 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -1,5 +1,6 @@ use std::{ collections::HashMap, + ffi::c_void, num::NonZeroI64, sync::{ atomic::{AtomicUsize, Ordering}, @@ -49,8 +50,6 @@ use scrap::CodecFormat; pub struct Remote { handler: Session, - video_queue_map: Arc>>>, - video_sender: MediaSender, audio_sender: MediaSender, receiver: mpsc::UnboundedReceiver, sender: mpsc::UnboundedSender, @@ -67,13 +66,11 @@ pub struct Remote { #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] client_conn_id: i32, // used for file clipboard data_count: Arc, - frame_count_map: Arc>>, video_format: CodecFormat, elevation_requested: bool, - fps_control: FpsControl, - decode_fps: Arc>>, - chroma: Arc>>, peer_info: ParsedPeerInfo, + video_threads: HashMap, + chroma: Arc>>, } #[derive(Default)] @@ -94,20 +91,12 @@ impl ParsedPeerInfo { impl Remote { pub fn new( handler: Session, - video_queue: Arc>>>, - video_sender: MediaSender, - audio_sender: MediaSender, receiver: mpsc::UnboundedReceiver, sender: mpsc::UnboundedSender, - frame_count_map: Arc>>, - decode_fps: Arc>>, - chroma: Arc>>, ) -> Self { Self { handler, - video_queue_map: video_queue, - video_sender, - audio_sender, + audio_sender: crate::client::start_audio_thread(), receiver, sender, read_jobs: Vec::new(), @@ -120,15 +109,13 @@ impl Remote { #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] client_conn_id: 0, data_count: Arc::new(AtomicUsize::new(0)), - frame_count_map, video_format: CodecFormat::Unknown, stop_voice_call_sender: None, voice_call_request_timestamp: None, elevation_requested: false, - fps_control: Default::default(), - decode_fps, - chroma, peer_info: Default::default(), + video_threads: Default::default(), + chroma: Default::default(), } } @@ -250,7 +237,6 @@ impl Remote { } } _ = status_timer.tick() => { - self.fps_control(direct); let elapsed = fps_instant.elapsed().as_millis(); if elapsed < 1000 { continue; @@ -260,14 +246,14 @@ impl Remote { speed = speed * 1000 / elapsed as usize; let speed = format!("{:.2}kB/s", speed as f32 / 1024 as f32); - let mut frame_count_map_write = self.frame_count_map.write().unwrap(); - let frame_count_map = frame_count_map_write.clone(); - frame_count_map_write.values_mut().for_each(|v| *v = 0); - drop(frame_count_map_write); - let fps = frame_count_map.iter().map(|(k, v)| { + let fps = self.video_threads.iter().map(|(k, v)| { // Correcting the inaccuracy of status_timer - (k.clone(), (*v as i32) * 1000 / elapsed as i32) + (k.clone(), (*v.frame_count.read().unwrap() as i32) * 1000 / elapsed as i32) }).collect::>(); + self.video_threads.iter().for_each(|(_, v)| { + *v.frame_count.write().unwrap() = 0; + }); + self.fps_control(direct, fps.clone()); let chroma = self.chroma.read().unwrap().clone(); let chroma = match chroma { Some(Chroma::I444) => "4:4:4", @@ -275,10 +261,16 @@ impl Remote { None => "-", }; let chroma = Some(chroma.to_string()); + let codec_format = if self.video_format == CodecFormat::Unknown { + None + } else { + Some(self.video_format.clone()) + }; self.handler.update_quality_status(QualityStatus { speed: Some(speed), fps, chroma, + codec_format, ..Default::default() }); } @@ -498,6 +490,22 @@ impl Remote { self.check_clipboard_file_context(); } Data::Message(msg) => { + match &msg.union { + Some(message::Union::Misc(misc)) => match misc.union { + Some(misc::Union::RefreshVideo(_)) => { + self.video_threads.iter().for_each(|(_, v)| { + *v.discard_queue.write().unwrap() = true; + }); + } + Some(misc::Union::RefreshVideoDisplay(display)) => { + if let Some(v) = self.video_threads.get_mut(&(display as usize)) { + *v.discard_queue.write().unwrap() = true; + } + } + _ => {} + }, + _ => {} + } allow_err!(peer.send(&msg).await); } Data::SendFiles((id, path, to, file_num, include_hidden, is_remote)) => { @@ -838,7 +846,10 @@ impl Remote { } } Data::RecordScreen(start) => { - let _ = self.video_sender.send(MediaData::RecordScreen(start)); + self.handler.lc.write().unwrap().record = start; + for (_, v) in self.video_threads.iter_mut() { + v.video_sender.send(MediaData::RecordScreen(start)).ok(); + } } Data::ElevateDirect => { let mut request = ElevationRequest::new(); @@ -881,9 +892,18 @@ impl Remote { .on_voice_call_closed("Closed manually by the peer"); allow_err!(peer.send(&msg).await); } - Data::ResetDecoder(display) => { - self.video_sender.send(MediaData::Reset(display)).ok(); - } + Data::ResetDecoder(display) => match display { + Some(display) => { + if let Some(v) = self.video_threads.get_mut(&display) { + v.video_sender.send(MediaData::Reset).ok(); + } + } + None => { + for (_, v) in self.video_threads.iter_mut() { + v.video_sender.send(MediaData::Reset).ok(); + } + } + }, _ => {} } true @@ -1011,100 +1031,115 @@ impl Remote { } } + // Currently, this function only considers decoding speed and queue length, not network delay. + // The controlled end can consider auto fps as the maximum decoding fps. #[inline] - fn fps_control(&mut self, direct: bool) { + fn fps_control(&mut self, direct: bool, real_fps_map: HashMap) { + self.video_threads.iter_mut().for_each(|(k, v)| { + let real_fps = real_fps_map.get(k).cloned().unwrap_or_default(); + if real_fps == 0 { + v.fps_control.inactive_counter += 1; + } else { + v.fps_control.inactive_counter = 0; + } + }); let custom_fps = self.handler.lc.read().unwrap().custom_fps.clone(); let custom_fps = custom_fps.lock().unwrap().clone(); let mut custom_fps = custom_fps.unwrap_or(30); if custom_fps < 5 || custom_fps > 120 { custom_fps = 30; } - let ctl = &mut self.fps_control; - let len = self - .video_queue_map - .read() - .unwrap() + let inactive_threshold = 15; + let max_queue_len = self + .video_threads .iter() - .map(|v| v.1.len()) + .map(|v| v.1.video_queue.read().unwrap().len()) .max() .unwrap_or_default(); - let decode_fps = self.decode_fps.read().unwrap().clone(); - let Some(mut decode_fps) = decode_fps else { + let min_decode_fps = self + .video_threads + .iter() + .filter(|v| v.1.fps_control.inactive_counter < inactive_threshold) + .map(|v| *v.1.decode_fps.read().unwrap()) + .min() + .flatten(); + let Some(min_decode_fps) = min_decode_fps else { return; }; - if cfg!(feature = "flutter") { - let active_displays = ctl - .last_active_time - .iter() - .filter(|t| t.1.elapsed().as_secs() < 5) - .count(); - if active_displays > 1 { - decode_fps = decode_fps / active_displays; - } - } let mut limited_fps = if direct { - decode_fps * 9 / 10 // 30 got 27 + min_decode_fps * 9 / 10 // 30 got 27 } else { - decode_fps * 4 / 5 // 30 got 24 + min_decode_fps * 4 / 5 // 30 got 24 }; if limited_fps > custom_fps { limited_fps = custom_fps; } let last_auto_fps = self.handler.lc.read().unwrap().last_auto_fps.clone(); - let should_decrease = (len > 1 - && last_auto_fps.clone().unwrap_or(custom_fps as _) > limited_fps) - || len > std::cmp::max(1, limited_fps / 2); - - // increase judgement - if len <= 1 { - if ctl.idle_counter < usize::MAX { + let displays = self.video_threads.keys().cloned().collect::>(); + let mut fps_trending = |display: usize| { + let thread = self.video_threads.get_mut(&display)?; + let ctl = &mut thread.fps_control; + let len = thread.video_queue.read().unwrap().len(); + let decode_fps = thread.decode_fps.read().unwrap().clone()?; + let last_auto_fps = last_auto_fps.clone().unwrap_or(custom_fps as _); + if ctl.inactive_counter > inactive_threshold { + return None; + } + if len > 1 && last_auto_fps > limited_fps || len > std::cmp::max(1, decode_fps / 2) { + ctl.idle_counter = 0; + return Some(false); + } + if len <= 1 { ctl.idle_counter += 1; + if ctl.idle_counter > 3 && last_auto_fps + 3 <= limited_fps { + return Some(true); + } } - } else { - ctl.idle_counter = 0; - } - let mut should_increase = false; - if let Some(last_auto_fps) = last_auto_fps.clone() { - // ever set - if last_auto_fps + 3 <= limited_fps && ctl.idle_counter > 3 { - // limited_fps is 3 larger than last set, and idle time is more than 3 seconds - should_increase = true; + if len > 1 { + ctl.idle_counter = 0; } - } + None + }; + let trendings: Vec<_> = displays.iter().map(|k| fps_trending(*k)).collect(); + let should_decrease = trendings.iter().any(|v| *v == Some(false)); + let should_increase = !should_decrease && trendings.iter().any(|v| *v == Some(true)); if last_auto_fps.is_none() || should_decrease || should_increase { // limited_fps to ensure decoding is faster than encoding let mut auto_fps = limited_fps; - if should_decrease && limited_fps < len { + if should_decrease && limited_fps < max_queue_len { auto_fps = limited_fps / 2; } if auto_fps < 1 { auto_fps = 1; } - let mut misc = Misc::new(); - misc.set_option(OptionMessage { - custom_fps: auto_fps as _, - ..Default::default() - }); - let mut msg = Message::new(); - msg.set_misc(misc); - self.sender.send(Data::Message(msg)).ok(); - log::info!("Set fps to {}", auto_fps); - ctl.last_queue_size = len; - self.handler.lc.write().unwrap().last_auto_fps = Some(auto_fps); + if Some(auto_fps) != last_auto_fps { + let mut misc = Misc::new(); + misc.set_option(OptionMessage { + custom_fps: auto_fps as _, + ..Default::default() + }); + let mut msg = Message::new(); + msg.set_misc(misc); + self.sender.send(Data::Message(msg)).ok(); + log::info!("Set fps to {}", auto_fps); + self.handler.lc.write().unwrap().last_auto_fps = Some(auto_fps); + } } // send refresh - for (display, video_queue) in self.video_queue_map.read().unwrap().iter() { - let tolerable = std::cmp::min(decode_fps, video_queue.capacity() / 2); + for (display, thread) in self.video_threads.iter_mut() { + let ctl = &mut thread.fps_control; + let video_queue = thread.video_queue.read().unwrap(); + let tolerable = std::cmp::min(min_decode_fps, video_queue.capacity() / 2); if ctl.refresh_times < 20 // enough && (video_queue.len() > tolerable - && (ctl.refresh_times == 0 || ctl.last_refresh_instant.elapsed().as_secs() > 10)) + && (ctl.refresh_times == 0 || ctl.last_refresh_instant.map(|t|t.elapsed().as_secs() > 10).unwrap_or(false))) { // Refresh causes client set_display, left frames cause flickering. - while let Some(_) = video_queue.pop() {} + drop(video_queue); self.handler.refresh_video(*display as _); log::info!("Refresh display {} to reduce delay", display); ctl.refresh_times += 1; - ctl.last_refresh_instant = Instant::now(); + ctl.last_refresh_instant = Some(Instant::now()); } } } @@ -1120,43 +1155,29 @@ impl Remote { self.send_toggle_virtual_display_msg(peer).await; self.send_toggle_privacy_mode_msg(peer).await; } - let incoming_format = CodecFormat::from(&vf); - if self.video_format != incoming_format { - self.video_format = incoming_format.clone(); - self.handler.update_quality_status(QualityStatus { - codec_format: Some(incoming_format), - ..Default::default() - }) - }; + self.video_format = CodecFormat::from(&vf); let display = vf.display as usize; - let mut video_queue_write = self.video_queue_map.write().unwrap(); - if !video_queue_write.contains_key(&display) { - video_queue_write.insert( - display, - ArrayQueue::::new(crate::client::VIDEO_QUEUE_SIZE), - ); + if !self.video_threads.contains_key(&display) { + self.new_video_thread(display); } + let Some(thread) = self.video_threads.get_mut(&display) else { + return true; + }; if Self::contains_key_frame(&vf) { - if let Some(video_queue) = video_queue_write.get_mut(&display) { - while let Some(_) = video_queue.pop() {} - } - self.video_sender + thread + .video_sender .send(MediaData::VideoFrame(Box::new(vf))) .ok(); } else { - if let Some(video_queue) = video_queue_write.get_mut(&display) { - if video_queue.force_push(vf).is_some() { - while let Some(_) = video_queue.pop() {} - self.handler.refresh_video(display as _); - } else { - self.video_sender.send(MediaData::VideoQueue(display)).ok(); - } + let video_queue = thread.video_queue.read().unwrap(); + if video_queue.force_push(vf).is_some() { + drop(video_queue); + self.handler.refresh_video(display as _); + } else { + thread.video_sender.send(MediaData::VideoQueue).ok(); } } - self.fps_control - .last_active_time - .insert(display, Instant::now()); } Some(message::Union::Hash(hash)) => { self.handler @@ -1470,9 +1491,10 @@ impl Remote { } Some(misc::Union::SwitchDisplay(s)) => { self.handler.handle_peer_switch_display(&s); - self.video_sender - .send(MediaData::Reset(Some(s.display as _))) - .ok(); + if let Some(thread) = self.video_threads.get_mut(&(s.display as usize)) { + thread.video_sender.send(MediaData::Reset).ok(); + } + if s.width > 0 && s.height > 0 { self.handler.set_display( s.x, @@ -1920,6 +1942,53 @@ impl Remote { }); } } + + fn new_video_thread(&mut self, display: usize) { + let video_queue = Arc::new(RwLock::new(ArrayQueue::new(client::VIDEO_QUEUE_SIZE))); + let (video_sender, video_receiver) = std::sync::mpsc::channel::(); + let decode_fps = Arc::new(RwLock::new(None)); + let frame_count = Arc::new(RwLock::new(0)); + let discard_queue = Arc::new(RwLock::new(false)); + let video_thread = VideoThread { + video_queue: video_queue.clone(), + video_sender, + decode_fps: decode_fps.clone(), + frame_count: frame_count.clone(), + fps_control: Default::default(), + discard_queue: discard_queue.clone(), + }; + let handler = self.handler.ui_handler.clone(); + crate::client::start_video_thread( + self.handler.clone(), + display, + video_receiver, + video_queue, + decode_fps, + self.chroma.clone(), + discard_queue, + move |display: usize, + data: &mut scrap::ImageRgb, + _texture: *mut c_void, + pixelbuffer: bool| { + *frame_count.write().unwrap() += 1; + if pixelbuffer { + handler.on_rgba(display, data); + } else { + #[cfg(all(feature = "vram", feature = "flutter"))] + handler.on_texture(display, _texture); + } + }, + ); + self.video_threads.insert(display, video_thread); + let auto_record = self.handler.lc.read().unwrap().record; + if auto_record && self.video_threads.len() == 1 { + let mut misc = Misc::new(); + misc.set_client_record_status(true); + let mut msg = Message::new(); + msg.set_misc(misc); + self.sender.send(Data::Message(msg)).ok(); + } + } } struct RemoveJob { @@ -1952,22 +2021,19 @@ impl RemoveJob { } } +#[derive(Debug, Default)] struct FpsControl { - last_queue_size: usize, refresh_times: usize, - last_refresh_instant: Instant, + last_refresh_instant: Option, idle_counter: usize, - last_active_time: HashMap, + inactive_counter: usize, } -impl Default for FpsControl { - fn default() -> Self { - Self { - last_queue_size: Default::default(), - refresh_times: Default::default(), - last_refresh_instant: Instant::now(), - idle_counter: 0, - last_active_time: Default::default(), - } - } +struct VideoThread { + video_queue: Arc>>, + video_sender: MediaSender, + decode_fps: Arc>>, + frame_count: Arc>, + discard_queue: Arc>, + fps_control: FpsControl, } diff --git a/src/server/video_service.rs b/src/server/video_service.rs index e6eed86cc5b0..eceaf69c1a6c 100644 --- a/src/server/video_service.rs +++ b/src/server/video_service.rs @@ -728,13 +728,7 @@ fn setup_encoder( ); Encoder::set_fallback(&encoder_cfg); let codec_format = Encoder::negotiated_codec(); - let recorder = get_recorder( - c.width, - c.height, - &codec_format, - record_incoming, - display_idx, - ); + let recorder = get_recorder(record_incoming, display_idx); let use_i444 = Encoder::use_i444(&encoder_cfg); let encoder = Encoder::new(encoder_cfg.clone(), use_i444)?; Ok((encoder, encoder_cfg, codec_format, use_i444, recorder)) @@ -816,13 +810,7 @@ fn get_encoder_config( } } -fn get_recorder( - width: usize, - height: usize, - codec_format: &CodecFormat, - record_incoming: bool, - display: usize, -) -> Arc>> { +fn get_recorder(record_incoming: bool, display: usize) -> Arc>> { #[cfg(windows)] let root = crate::platform::is_root(); #[cfg(not(windows))] diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 4bd4b6e2d148..5194b12db867 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -35,8 +35,8 @@ use hbb_common::{ use crate::client::io_loop::Remote; use crate::client::{ check_if_retry, handle_hash, handle_login_error, handle_login_from_ui, handle_test_delay, - input_os_password, send_mouse, send_pointer_device_event, start_video_audio_threads, - FileManager, Key, LoginConfigHandler, QualityStatus, KEY_MAP, + input_os_password, send_mouse, send_pointer_device_event, FileManager, Key, LoginConfigHandler, + QualityStatus, KEY_MAP, }; #[cfg(not(any(target_os = "android", target_os = "ios")))] use crate::common::GrabState; @@ -1848,40 +1848,7 @@ pub async fn io_loop(handler: Session, round: u32) { } return; } - let frame_count_map: Arc>> = Default::default(); - let frame_count_map_cl = frame_count_map.clone(); - let ui_handler = handler.ui_handler.clone(); - let (video_sender, audio_sender, video_queue_map, decode_fps, chroma) = - start_video_audio_threads( - handler.clone(), - move |display: usize, - data: &mut scrap::ImageRgb, - _texture: *mut c_void, - pixelbuffer: bool| { - let mut write_lock = frame_count_map_cl.write().unwrap(); - let count = write_lock.get(&display).unwrap_or(&0) + 1; - write_lock.insert(display, count); - drop(write_lock); - if pixelbuffer { - ui_handler.on_rgba(display, data); - } else { - #[cfg(all(feature = "vram", feature = "flutter"))] - ui_handler.on_texture(display, _texture); - } - }, - ); - - let mut remote = Remote::new( - handler, - video_queue_map, - video_sender, - audio_sender, - receiver, - sender, - frame_count_map, - decode_fps, - chroma, - ); + let mut remote = Remote::new(handler, receiver, sender); remote.io_loop(&key, &token, round).await; remote.sync_jobs_status_to_local().await; } From b487f297b8917a73eedcea265264ba6647b6db7b Mon Sep 17 00:00:00 2001 From: rustdesk Date: Fri, 22 Nov 2024 16:58:08 +0900 Subject: [PATCH 399/541] flutter 3.24.5 --- .github/workflows/flutter-build.yml | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/flutter-build.yml b/.github/workflows/flutter-build.yml index fd169e1dd9b2..51c8ebcba3a4 100644 --- a/.github/workflows/flutter-build.yml +++ b/.github/workflows/flutter-build.yml @@ -24,8 +24,8 @@ env: SCITER_ARMV7_CMAKE_VERSION: "3.29.7" SCITER_NASM_DEBVERSION: "2.14-1" LLVM_VERSION: "15.0.6" - FLUTTER_VERSION: "3.24.4" - ANDROID_FLUTTER_VERSION: "3.24.4" + FLUTTER_VERSION: "3.24.5" + ANDROID_FLUTTER_VERSION: "3.24.5" # for arm64 linux because official Dart SDK does not work FLUTTER_ELINUX_VERSION: "3.16.9" TAG_NAME: "${{ inputs.upload-tag }}" @@ -118,7 +118,7 @@ jobs: run: | cp .github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff $(dirname $(dirname $(which flutter))) cd $(dirname $(dirname $(which flutter))) - [[ "3.24.4" == ${{env.FLUTTER_VERSION}} ]] && git apply flutter_3.24.4_dropdown_menu_enableFilter.diff + [[ "3.24.5" == ${{env.FLUTTER_VERSION}} ]] && git apply flutter_3.24.4_dropdown_menu_enableFilter.diff - name: Install Rust toolchain uses: dtolnay/rust-toolchain@v1 @@ -495,7 +495,7 @@ jobs: - name: Patch flutter run: | cd $(dirname $(dirname $(which flutter))) - [[ "3.24.4" == ${{env.FLUTTER_VERSION}} ]] && git apply ${{ github.workspace }}/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff + [[ "3.24.5" == ${{env.FLUTTER_VERSION}} ]] && git apply ${{ github.workspace }}/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff - name: Setup vcpkg with Github Actions binary cache uses: lukka/run-vcpkg@v11 @@ -701,7 +701,7 @@ jobs: - name: Patch flutter run: | cd $(dirname $(dirname $(which flutter))) - [[ "3.24.4" == ${{env.FLUTTER_VERSION}} ]] && git apply ${{ github.workspace }}/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff + [[ "3.24.5" == ${{env.FLUTTER_VERSION}} ]] && git apply ${{ github.workspace }}/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff - name: Workaround for flutter issue shell: bash @@ -942,7 +942,7 @@ jobs: - name: Patch flutter run: | cd $(dirname $(dirname $(which flutter))) - [[ "3.24.4" == ${{env.ANDROID_FLUTTER_VERSION}} ]] && git apply ${{ github.workspace }}/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff + [[ "3.24.5" == ${{env.ANDROID_FLUTTER_VERSION}} ]] && git apply ${{ github.workspace }}/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff - uses: nttld/setup-ndk@v1 id: setup-ndk @@ -1219,7 +1219,7 @@ jobs: - name: Patch flutter run: | cd $(dirname $(dirname $(which flutter))) - [[ "3.24.4" == ${{env.ANDROID_FLUTTER_VERSION}} ]] && git apply ${{ github.workspace }}/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff + [[ "3.24.5" == ${{env.ANDROID_FLUTTER_VERSION}} ]] && git apply ${{ github.workspace }}/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff - name: Restore bridge files uses: actions/download-artifact@master @@ -1570,7 +1570,7 @@ jobs: ;; esac - if [[ "3.24.4" == ${{ env.FLUTTER_VERSION }} ]]; then + if [[ "3.24.5" == ${{ env.FLUTTER_VERSION }} ]]; then case ${{ matrix.job.arch }} in aarch64) pushd /opt/flutter-elinux/flutter @@ -2057,7 +2057,7 @@ jobs: shell: bash run: | cd $(dirname $(dirname $(which flutter))) - [[ "3.24.4" == ${{env.FLUTTER_VERSION}} ]] && git apply ${{ github.workspace }}/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff + [[ "3.24.5" == ${{env.FLUTTER_VERSION}} ]] && git apply ${{ github.workspace }}/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff # https://rustdesk.com/docs/en/dev/build/web/ - name: Build web From 28d38cd71d0f5e3e4ac3630efda153b9dcfca4b9 Mon Sep 17 00:00:00 2001 From: 21pages Date: Fri, 22 Nov 2024 17:19:22 +0800 Subject: [PATCH 400/541] avoid invalid recording files and fix removing little recording file (#10012) Signed-off-by: 21pages --- libs/scrap/src/common/record.rs | 37 ++++++++++++++++++++++++--------- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/libs/scrap/src/common/record.rs b/libs/scrap/src/common/record.rs index c53b7743147f..6a1a6d60fcb9 100644 --- a/libs/scrap/src/common/record.rs +++ b/libs/scrap/src/common/record.rs @@ -158,6 +158,7 @@ impl Recorder { #[cfg(not(feature = "hwcodec"))] _ => bail!("unsupported codec type"), }; + // pts is None when new inner is created self.pts = None; self.send_state(RecordState::NewFile(ctx2.filename.clone())); } @@ -194,33 +195,33 @@ impl Recorder { match frame { video_frame::Union::Vp8s(vp8s) => { for f in vp8s.frames.iter() { - self.check_pts(f.pts, w, h, format)?; + self.check_pts(f.pts, f.key, w, h, format)?; self.as_mut().map(|x| x.write_video(f)); } } video_frame::Union::Vp9s(vp9s) => { for f in vp9s.frames.iter() { - self.check_pts(f.pts, w, h, format)?; + self.check_pts(f.pts, f.key, w, h, format)?; self.as_mut().map(|x| x.write_video(f)); } } video_frame::Union::Av1s(av1s) => { for f in av1s.frames.iter() { - self.check_pts(f.pts, w, h, format)?; + self.check_pts(f.pts, f.key, w, h, format)?; self.as_mut().map(|x| x.write_video(f)); } } #[cfg(feature = "hwcodec")] video_frame::Union::H264s(h264s) => { for f in h264s.frames.iter() { - self.check_pts(f.pts, w, h, format)?; + self.check_pts(f.pts, f.key, w, h, format)?; self.as_mut().map(|x| x.write_video(f)); } } #[cfg(feature = "hwcodec")] video_frame::Union::H265s(h265s) => { for f in h265s.frames.iter() { - self.check_pts(f.pts, w, h, format)?; + self.check_pts(f.pts, f.key, w, h, format)?; self.as_mut().map(|x| x.write_video(f)); } } @@ -230,8 +231,18 @@ impl Recorder { Ok(()) } - fn check_pts(&mut self, pts: i64, w: usize, h: usize, format: CodecFormat) -> ResultType<()> { + fn check_pts( + &mut self, + pts: i64, + key: bool, + w: usize, + h: usize, + format: CodecFormat, + ) -> ResultType<()> { // https://stackoverflow.com/questions/76379101/how-to-create-one-playable-webm-file-from-two-different-video-tracks-with-same-c + if self.pts.is_none() && !key { + bail!("first frame is not key frame"); + } let old_pts = self.pts; self.pts = Some(pts); if old_pts.clone().unwrap_or_default() > pts { @@ -342,7 +353,7 @@ impl Drop for WebmRecorder { #[cfg(feature = "hwcodec")] struct HwRecorder { - muxer: Muxer, + muxer: Option, ctx: RecorderContext, ctx2: RecorderContext2, written: bool, @@ -362,7 +373,7 @@ impl RecorderApi for HwRecorder { }) .map_err(|_| anyhow!("Failed to create hardware muxer"))?; Ok(HwRecorder { - muxer, + muxer: Some(muxer), ctx, ctx2, written: false, @@ -376,7 +387,11 @@ impl RecorderApi for HwRecorder { self.key = true; } if self.key { - let ok = self.muxer.write_video(&frame.data, frame.key).is_ok(); + let ok = self + .muxer + .as_mut() + .map(|m| m.write_video(&frame.data, frame.key).is_ok()) + .unwrap_or_default(); if ok { self.written = true; } @@ -390,9 +405,11 @@ impl RecorderApi for HwRecorder { #[cfg(feature = "hwcodec")] impl Drop for HwRecorder { fn drop(&mut self) { - self.muxer.write_tail().ok(); + self.muxer.as_mut().map(|m| m.write_tail().ok()); let mut state = RecordState::WriteTail; if !self.written || self.start.elapsed().as_secs() < MIN_SECS { + // The process cannot access the file because it is being used by another process + self.muxer = None; std::fs::remove_file(&self.ctx2.filename).ok(); state = RecordState::RemoveFile; } From ab6a6ca17d9578aac13dfbb3ef2f4abd84a4ce37 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Sat, 23 Nov 2024 08:38:30 +0800 Subject: [PATCH 401/541] fix: ci macos (#10020) Signed-off-by: fufesou --- .github/workflows/flutter-build.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/flutter-build.yml b/.github/workflows/flutter-build.yml index 51c8ebcba3a4..f132d43ace20 100644 --- a/.github/workflows/flutter-build.yml +++ b/.github/workflows/flutter-build.yml @@ -690,7 +690,13 @@ jobs: - name: Install build runtime run: | - brew install llvm create-dmg nasm cmake gcc wget ninja pkg-config + brew install llvm create-dmg nasm cmake gcc wget ninja + # pkg-config is handled in a separate step, because it may be already installed by `macos-latest`(14.7.1) runner + if command -v pkg-config &>/dev/null; then + echo "pkg-config is already installed" + else + brew install pkg-config + fi - name: Install flutter uses: subosito/flutter-action@v2 From 02c274aeb61825acc756cce36ec95ec84fcd9447 Mon Sep 17 00:00:00 2001 From: Andrzej Rudnik Date: Sat, 23 Nov 2024 01:41:25 +0100 Subject: [PATCH 402/541] Updated Polish translation (#10019) * Update pl.rs * Update README-PL.md --- docs/README-PL.md | 4 ++++ src/lang/pl.rs | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/README-PL.md b/docs/README-PL.md index 4d3464d41ae6..295564457b86 100644 --- a/docs/README-PL.md +++ b/docs/README-PL.md @@ -164,3 +164,7 @@ Upewnij się, że uruchamiasz te polecenia z katalogu głównego repozytorium Ru ![image](https://user-images.githubusercontent.com/71636191/113112857-3fbd5d80-923c-11eb-9836-768325faf906.png) ![image](https://user-images.githubusercontent.com/71636191/135385039-38fdbd72-379a-422d-b97f-33df71fb1cec.png) + +## [Serwery publiczne](#public-servers) + +RustDesk jest obsługiwany przez bezpłatne serwer w Unii Europejskiej, uprzejmie dostarczony przez [Codext GmbH](https://codext.link/rustdesk?utm_source=github) diff --git a/src/lang/pl.rs b/src/lang/pl.rs index fb77c9abb1a8..b6cc5318ab86 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -653,6 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", "Wyślij folder"), ("Upload files", "Wyślij pliki"), ("Clipboard is synchronized", "Schowek jest zsynchronizowany"), - ("Update client clipboard", ""), + ("Update client clipboard", "Uaktualnij schowek klienta"), ].iter().cloned().collect(); } From 02b046bdbfad52be19e30e5cce76aecf15bfa269 Mon Sep 17 00:00:00 2001 From: 21pages Date: Sat, 23 Nov 2024 16:36:13 +0800 Subject: [PATCH 403/541] fix hwcodec ram quality change not work (#10024) Signed-off-by: 21pages --- libs/scrap/src/common/hwcodec.rs | 4 ++-- libs/scrap/src/common/vram.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/libs/scrap/src/common/hwcodec.rs b/libs/scrap/src/common/hwcodec.rs index 2a83afacc8e4..d929024d84eb 100644 --- a/libs/scrap/src/common/hwcodec.rs +++ b/libs/scrap/src/common/hwcodec.rs @@ -70,7 +70,7 @@ impl EncoderApi for HwRamEncoder { let b = Self::convert_quality(&config.name, config.quality); let base_bitrate = base_bitrate(config.width as _, config.height as _); let mut bitrate = base_bitrate * b / 100; - if base_bitrate <= 0 { + if bitrate <= 0 { bitrate = base_bitrate; } bitrate = Self::check_bitrate_range(&config, bitrate); @@ -180,7 +180,7 @@ impl EncoderApi for HwRamEncoder { let b = Self::convert_quality(&self.config.name, quality); let mut bitrate = base_bitrate(self.config.width as _, self.config.height as _) * b / 100; if bitrate > 0 { - bitrate = Self::check_bitrate_range(&self.config, self.bitrate); + bitrate = Self::check_bitrate_range(&self.config, bitrate); self.encoder.set_bitrate(bitrate as _).ok(); self.bitrate = bitrate; } diff --git a/libs/scrap/src/common/vram.rs b/libs/scrap/src/common/vram.rs index aae961df6143..eb3b8e1ce39e 100644 --- a/libs/scrap/src/common/vram.rs +++ b/libs/scrap/src/common/vram.rs @@ -64,7 +64,7 @@ impl EncoderApi for VRamEncoder { let b = Self::convert_quality(config.quality, &config.feature); let base_bitrate = base_bitrate(config.width as _, config.height as _); let mut bitrate = base_bitrate * b / 100; - if base_bitrate <= 0 { + if bitrate <= 0 { bitrate = base_bitrate; } let gop = config.keyframe_interval.unwrap_or(MAX_GOP as _) as i32; From 0973f51df9bf1f68c1e6f09f5204c8f7471dc2ff Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Sat, 23 Nov 2024 18:41:27 +0800 Subject: [PATCH 404/541] feat: macos, audio, loopback (#10025) Signed-off-by: fufesou --- .github/workflows/flutter-build.yml | 12 ++++- Cargo.lock | 22 +++++++-- Cargo.toml | 3 +- build.py | 9 ++++ flutter/lib/common/widgets/audio_input.dart | 8 ++-- src/common.rs | 2 +- src/flutter_ffi.rs | 17 ++++--- src/server/audio_service.rs | 51 ++++++++++++++++++++- src/ui_interface.rs | 2 + 9 files changed, 105 insertions(+), 21 deletions(-) diff --git a/.github/workflows/flutter-build.yml b/.github/workflows/flutter-build.yml index f132d43ace20..22b555764c26 100644 --- a/.github/workflows/flutter-build.yml +++ b/.github/workflows/flutter-build.yml @@ -20,6 +20,7 @@ on: env: SCITER_RUST_VERSION: "1.75" # https://github.com/rustdesk/rustdesk/discussions/7503, also 1.78 has ABI change which causes our sciter version not working, https://blog.rust-lang.org/2024/03/30/i128-layout-update.html RUST_VERSION: "1.75" # sciter failed on m1 with 1.78 because of https://blog.rust-lang.org/2024/03/30/i128-layout-update.html + MAC_RUST_VERSION: "1.81" # 1.81 is requred for macos, because of https://github.com/yury/cidre requires 1.81 CARGO_NDK_VERSION: "3.1.2" SCITER_ARMV7_CMAKE_VERSION: "3.29.7" SCITER_NASM_DEBVERSION: "2.14-1" @@ -641,7 +642,7 @@ jobs: target: aarch64-apple-darwin, os: macos-latest, # extra-build-args: "--disable-flutter-texture-render", # disable this for mac, because we see a lot of users reporting flickering both on arm and x64, and we can not confirm if texture rendering has better performance if htere is no vram, https://github.com/rustdesk/rustdesk/issues/6296 - extra-build-args: "", + extra-build-args: "--screencapturekit", arch: aarch64, } steps: @@ -720,7 +721,7 @@ jobs: - name: Install Rust toolchain uses: dtolnay/rust-toolchain@v1 with: - toolchain: ${{ env.RUST_VERSION }} + toolchain: ${{ env.MAC_RUST_VERSION }} targets: ${{ matrix.job.target }} components: "rustfmt" @@ -767,6 +768,13 @@ jobs: - name: Build rustdesk run: | + if [ "${{ matrix.job.target }}" = "aarch64-apple-darwin" ]; then + MIN_MACOS_VERSION="12.3" + sed -i -e "s/MACOSX_DEPLOYMENT_TARGET\=[0-9]*.[0-9]*/MACOSX_DEPLOYMENT_TARGET=${MIN_MACOS_VERSION}/" build.py + sed -i -e "s/platform :osx, '.*'/platform :osx, '${MIN_MACOS_VERSION}'/" flutter/macos/Podfile + sed -i -e "s/osx_minimum_system_version = \"[0-9]*.[0-9]*\"/osx_minimum_system_version = \"${MIN_MACOS_VERSION}\"/" Cargo.toml + sed -i -e "s/MACOSX_DEPLOYMENT_TARGET = [0-9]*.[0-9]*;/MACOSX_DEPLOYMENT_TARGET = ${MIN_MACOS_VERSION};/" flutter/macos/Runner.xcodeproj/project.pbxproj + fi ./build.py --flutter --hwcodec ${{ matrix.job.extra-build-args }} - name: create unsigned dmg diff --git a/Cargo.lock b/Cargo.lock index 9b450437d97e..6e2e56b3bcb5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -893,6 +893,20 @@ dependencies = [ "regex", ] +[[package]] +name = "cidre" +version = "0.4.0" +source = "git+https://github.com/yury/cidre.git?rev=f05c428#f05c4288f9870c9fab53272ddafd6ec01c7b2dbf" +dependencies = [ + "cidre-macros", + "parking_lot", +] + +[[package]] +name = "cidre-macros" +version = "0.1.0" +source = "git+https://github.com/yury/cidre.git?rev=f05c428#f05c4288f9870c9fab53272ddafd6ec01c7b2dbf" + [[package]] name = "cipher" version = "0.4.4" @@ -1276,10 +1290,10 @@ dependencies = [ [[package]] name = "cpal" version = "0.15.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "873dab07c8f743075e57f524c583985fbaf745602acbe916a01539364369a779" +source = "git+https://github.com/rustdesk-org/cpal?branch=osx-screencapturekit#4d318ff778063ce14669fd4bd67a1673653fc6e5" dependencies = [ "alsa", + "cidre", "core-foundation-sys 0.8.6 (registry+https://github.com/rust-lang/crates.io-index)", "coreaudio-rs", "dasp_sample", @@ -3505,7 +3519,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e310b3a6b5907f99202fcdb4960ff45b93735d7c7d96b760fcff8db2dc0e103d" dependencies = [ "cfg-if 1.0.0", - "windows-targets 0.52.5", + "windows-targets 0.48.5", ] [[package]] @@ -4149,7 +4163,7 @@ version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "681030a937600a36906c185595136d26abfebb4aa9c65701cefcaf8578bb982b" dependencies = [ - "proc-macro-crate 2.0.2", + "proc-macro-crate 1.3.1", "proc-macro2 1.0.86", "quote 1.0.36", "syn 2.0.68", diff --git a/Cargo.toml b/Cargo.toml index eca0fc55c30b..dbf819bf8389 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,6 +36,7 @@ unix-file-copy-paste = [ "dep:once_cell", "clipboard/unix-file-copy-paste", ] +screencapturekit = ["cpal/screencapturekit"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -77,7 +78,7 @@ fon = "0.6" zip = "0.6" shutdown_hooks = "0.1" totp-rs = { version = "5.4", default-features = false, features = ["gen_secret", "otpauth"] } -cpal = "0.15" +cpal = { git = "https://github.com/rustdesk-org/cpal", branch = "osx-screencapturekit" } ringbuf = "0.3" [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] diff --git a/build.py b/build.py index b7ea0a1ef68d..174bd18ebdf0 100755 --- a/build.py +++ b/build.py @@ -143,6 +143,12 @@ def make_parser(): "--package", type=str ) + if osx: + parser.add_argument( + '--screencapturekit', + action='store_true', + help='Enable feature screencapturekit' + ) return parser @@ -274,6 +280,9 @@ def get_features(args): features.append('flutter') if args.unix_file_copy_paste: features.append('unix-file-copy-paste') + if osx: + if args.screencapturekit: + features.append('screencapturekit') print("features:", features) return features diff --git a/flutter/lib/common/widgets/audio_input.dart b/flutter/lib/common/widgets/audio_input.dart index 1db4391270cb..1f8f1a8b94e6 100644 --- a/flutter/lib/common/widgets/audio_input.dart +++ b/flutter/lib/common/widgets/audio_input.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/models/platform_model.dart'; -const _kWindowsSystemSound = 'System Sound'; +const _kSystemSound = 'System Sound'; typedef AudioINputSetDevice = void Function(String device); typedef AudioInputBuilder = Widget Function( @@ -21,7 +21,7 @@ class AudioInput extends StatelessWidget { : super(key: key); static String getDefault() { - if (isWindows) return translate('System Sound'); + if (bind.mainAudioSupportLoopback()) return translate(_kSystemSound); return ''; } @@ -55,8 +55,8 @@ class AudioInput extends StatelessWidget { static Future> getDevicesInfo( bool isCm, bool isVoiceCall) async { List devices = (await bind.mainGetSoundInputs()).toList(); - if (isWindows) { - devices.insert(0, translate(_kWindowsSystemSound)); + if (bind.mainAudioSupportLoopback()) { + devices.insert(0, translate(_kSystemSound)); } String current = await getValue(isCm, isVoiceCall); return {'devices': devices, 'current': current}; diff --git a/src/common.rs b/src/common.rs index f53dd703fe6a..b1f97e27bdde 100644 --- a/src/common.rs +++ b/src/common.rs @@ -837,7 +837,7 @@ async fn check_software_update_() -> hbb_common::ResultType<()> { let _ = crate::flutter::push_global_event(crate::flutter::APP_TYPE_MAIN, data); } } - *SOFTWARE_UPDATE_URL.lock().unwrap() = response_url; + *SOFTWARE_UPDATE_URL.lock().unwrap() = response_url; } Ok(()) } diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index ba03bc761300..90dafccd027e 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -774,13 +774,6 @@ pub fn main_get_sound_inputs() -> Vec { vec![String::from("")] } -pub fn main_get_default_sound_input() -> Option { - #[cfg(not(any(target_os = "android", target_os = "ios")))] - return get_default_sound_input(); - #[cfg(any(target_os = "android", target_os = "ios"))] - None -} - pub fn main_get_login_device_info() -> SyncReturn { SyncReturn(get_login_device_info_json()) } @@ -2317,6 +2310,16 @@ pub fn session_request_new_display_init_msgs(session_id: SessionID, display: usi } } +pub fn main_audio_support_loopback() -> SyncReturn { + #[cfg(target_os = "windows")] + let is_surpport = true; + #[cfg(feature = "screencapturekit")] + let is_surpport = crate::audio_service::is_screen_capture_kit_available(); + #[cfg(not(any(target_os = "windows", feature = "screencapturekit")))] + let is_surpport = false; + SyncReturn(is_surpport) +} + #[cfg(target_os = "android")] pub mod server_side { use hbb_common::{config, log}; diff --git a/src/server/audio_service.rs b/src/server/audio_service.rs index 5d60abf6cb81..d1bb2d87842d 100644 --- a/src/server/audio_service.rs +++ b/src/server/audio_service.rs @@ -156,6 +156,14 @@ mod pa_impl { } } +#[inline] +#[cfg(feature = "screencapturekit")] +pub fn is_screen_capture_kit_available() -> bool { + cpal::available_hosts() + .iter() + .any(|host| *host == cpal::HostId::ScreenCaptureKit) +} + #[cfg(not(any(target_os = "linux", target_os = "android")))] mod cpal_impl { use self::service::{Reset, ServiceSwap}; @@ -170,6 +178,11 @@ mod cpal_impl { static ref INPUT_BUFFER: Arc>> = Default::default(); } + #[cfg(feature = "screencapturekit")] + lazy_static::lazy_static! { + static ref HOST_SCREEN_CAPTURE_KIT: Result = cpal::host_from_id(cpal::HostId::ScreenCaptureKit); + } + #[derive(Default)] pub struct State { stream: Option<(Box, Arc)>, @@ -246,6 +259,27 @@ mod cpal_impl { send_f32(&data, encoder, sp); } + #[cfg(feature = "screencapturekit")] + fn get_device() -> ResultType<(Device, SupportedStreamConfig)> { + let audio_input = super::get_audio_input(); + if !audio_input.is_empty() { + return get_audio_input(&audio_input); + } + if !is_screen_capture_kit_available() { + return get_audio_input(""); + } + let device = HOST_SCREEN_CAPTURE_KIT + .as_ref()? + .default_input_device() + .with_context(|| "Failed to get default input device for loopback")?; + let format = device + .default_input_config() + .map_err(|e| anyhow!(e)) + .with_context(|| "Failed to get input output format")?; + log::info!("Default input format: {:?}", format); + Ok((device, format)) + } + #[cfg(windows)] fn get_device() -> ResultType<(Device, SupportedStreamConfig)> { let audio_input = super::get_audio_input(); @@ -267,7 +301,7 @@ mod cpal_impl { Ok((device, format)) } - #[cfg(not(windows))] + #[cfg(not(any(windows, feature = "screencapturekit")))] fn get_device() -> ResultType<(Device, SupportedStreamConfig)> { let audio_input = super::get_audio_input(); get_audio_input(&audio_input) @@ -275,7 +309,20 @@ mod cpal_impl { fn get_audio_input(audio_input: &str) -> ResultType<(Device, SupportedStreamConfig)> { let mut device = None; - if !audio_input.is_empty() { + #[cfg(feature = "screencapturekit")] + if !audio_input.is_empty() && is_screen_capture_kit_available() { + for d in HOST_SCREEN_CAPTURE_KIT + .as_ref()? + .devices() + .with_context(|| "Failed to get audio devices")? + { + if d.name().unwrap_or("".to_owned()) == audio_input { + device = Some(d); + break; + } + } + } + if device.is_none() && !audio_input.is_empty() { for d in HOST .devices() .with_context(|| "Failed to get audio devices")? diff --git a/src/ui_interface.rs b/src/ui_interface.rs index bab54c79a3cc..caff46b84f33 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -321,6 +321,8 @@ pub fn get_sound_inputs() -> Vec { fn get_sound_inputs_() -> Vec { let mut out = Vec::new(); use cpal::traits::{DeviceTrait, HostTrait}; + // Do not use `cpal::host_from_id(cpal::HostId::ScreenCaptureKit)` for feature = "screencapturekit" + // Because we explicitly handle the "System Sound" device. let host = cpal::default_host(); if let Ok(devices) = host.devices() { for device in devices { From ac044c4049e028ebe8a00e9369af4dfcba0fcd6a Mon Sep 17 00:00:00 2001 From: RustDesk <71636191+rustdesk@users.noreply.github.com> Date: Sat, 23 Nov 2024 23:54:43 +0900 Subject: [PATCH 405/541] Update config.toml (#10029) --- .cargo/config.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.cargo/config.toml b/.cargo/config.toml index d971518eb1dd..a6219b20a1e8 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -12,3 +12,5 @@ rustflags = [ #rustflags = [ # "-C", "link-args=-Wl,-Bstatic -lc -Wl,-Bdynamic" #] +【net] +git-fetch-with-cli = true From b64f6271e2b8989759c4d09f6720a6efca52dc81 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Sun, 24 Nov 2024 00:08:39 +0900 Subject: [PATCH 406/541] typo --- .cargo/config.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.cargo/config.toml b/.cargo/config.toml index a6219b20a1e8..42a4adb55e1a 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -12,5 +12,5 @@ rustflags = [ #rustflags = [ # "-C", "link-args=-Wl,-Bstatic -lc -Wl,-Bdynamic" #] -【net] +[net] git-fetch-with-cli = true From 314c93b2107b4737570ecffdbd47d2e885505554 Mon Sep 17 00:00:00 2001 From: zuiyu <1542844298@qq.com> Date: Sat, 23 Nov 2024 23:09:11 +0800 Subject: [PATCH 407/541] Create empty dir on send files in local (#9993) * feat: Add empty dirs on sendfiles * Update connection.rs --------- Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com> --- flutter/lib/models/file_model.dart | 129 ++++++++++++++++++++++++++- flutter/lib/models/model.dart | 2 + libs/hbb_common/protos/message.proto | 12 +++ libs/hbb_common/src/fs.rs | 45 ++++++++++ src/client/file_trait.rs | 12 +++ src/client/io_loop.rs | 3 + src/common.rs | 35 +++++++- src/flutter.rs | 14 +++ src/flutter_ffi.rs | 23 ++++- src/ipc.rs | 4 + src/server/connection.rs | 11 +++ src/ui_cm_interface.rs | 26 ++++++ src/ui_session_interface.rs | 1 + 13 files changed, 311 insertions(+), 6 deletions(-) diff --git a/flutter/lib/models/file_model.dart b/flutter/lib/models/file_model.dart index 05c79ae86a75..d4ace7578e24 100644 --- a/flutter/lib/models/file_model.dart +++ b/flutter/lib/models/file_model.dart @@ -100,6 +100,10 @@ class FileModel { fileFetcher.tryCompleteTask(evt['value'], evt['is_local']); } + void receiveEmptyDirs(Map evt) { + fileFetcher.tryCompleteEmptyDirsTask(evt['value'], evt['is_local']); + } + Future postOverrideFileConfirm(Map evt) async { evtLoop.pushEvent( _FileDialogEvent(WeakReference(this), FileDialogType.overwrite, evt)); @@ -470,7 +474,8 @@ class FileController { } /// sendFiles from current side (FileController.isLocal) to other side (SelectedItems). - void sendFiles(SelectedItems items, DirectoryData otherSideData) { + Future sendFiles( + SelectedItems items, DirectoryData otherSideData) async { /// ignore wrong items side status if (items.isLocal != isLocal) { return; @@ -496,6 +501,42 @@ class FileController { debugPrint( "path: ${from.path}, toPath: $toPath, to: ${PathUtil.join(toPath, from.name, isWindows)}"); } + + if (!isLocal && + versionCmp(rootState.target!.ffiModel.pi.version, '1.3.3') < 0) { + return; + } + + final List entrys = items.items.toList(); + var isRemote = isLocal == true ? true : false; + + await Future.forEach(entrys, (Entry item) async { + if (!item.isDirectory) { + return; + } + + final List paths = []; + + final emptyDirs = + await fileFetcher.readEmptyDirs(item.path, isLocal, showHidden); + + if (emptyDirs.isEmpty) { + return; + } else { + for (var dir in emptyDirs) { + paths.add(dir.path); + } + } + + final dirs = paths.map((path) { + return PathUtil.getOtherSidePath(directory.value.path, path, + options.value.isWindows, toPath, isWindows); + }); + + for (var dir in dirs) { + createDirWithRemote(dir, isRemote); + } + }); } bool _removeCheckboxRemember = false; @@ -689,12 +730,16 @@ class FileController { sessionId: sessionId, actId: actId, path: path, isRemote: !isLocal); } - Future createDir(String path) async { + Future createDirWithRemote(String path, bool isRemote) async { bind.sessionCreateDir( sessionId: sessionId, actId: JobController.jobID.next(), path: path, - isRemote: !isLocal); + isRemote: isRemote); + } + + Future createDir(String path) async { + await createDirWithRemote(path, !isLocal); } Future renameAction(Entry item, bool isLocal) async { @@ -1064,6 +1109,7 @@ class JobResultListener { class FileFetcher { // Map> localTasks = {}; // now we only use read local dir sync Map> remoteTasks = {}; + Map>> remoteEmptyDirsTasks = {}; Map> readRecursiveTasks = {}; final GetSessionID getSessionID; @@ -1071,6 +1117,24 @@ class FileFetcher { FileFetcher(this.getSessionID); + Future> registerReadEmptyDirsTask( + bool isLocal, String path) { + // final jobs = isLocal?localJobs:remoteJobs; // maybe we will use read local dir async later + final tasks = remoteEmptyDirsTasks; // bypass now + if (tasks.containsKey(path)) { + throw "Failed to registerReadEmptyDirsTask, already have same read job"; + } + final c = Completer>(); + tasks[path] = c; + + Timer(Duration(seconds: 2), () { + tasks.remove(path); + if (c.isCompleted) return; + c.completeError("Failed to read empty dirs, timeout"); + }); + return c.future; + } + Future registerReadTask(bool isLocal, String path) { // final jobs = isLocal?localJobs:remoteJobs; // maybe we will use read local dir async later final tasks = remoteTasks; // bypass now @@ -1104,6 +1168,25 @@ class FileFetcher { return c.future; } + tryCompleteEmptyDirsTask(String? msg, String? isLocalStr) { + if (msg == null || isLocalStr == null) return; + late final Map>> tasks; + try { + final map = jsonDecode(msg); + final String path = map["path"]; + final List fdJsons = map["empty_dirs"]; + final List fds = + fdJsons.map((fdJson) => FileDirectory.fromJson(fdJson)).toList(); + + tasks = remoteEmptyDirsTasks; + final completer = tasks.remove(path); + + completer?.complete(fds); + } catch (e) { + debugPrint("tryCompleteJob err: $e"); + } + } + tryCompleteTask(String? msg, String? isLocalStr) { if (msg == null || isLocalStr == null) return; late final Map> tasks; @@ -1127,6 +1210,28 @@ class FileFetcher { } } + Future> readEmptyDirs( + String path, bool isLocal, bool showHidden) async { + try { + if (isLocal) { + final res = await bind.sessionReadLocalEmptyDirsRecursiveSync( + sessionId: sessionId, path: path, includeHidden: showHidden); + + final List fdJsons = jsonDecode(res); + + final List fds = + fdJsons.map((fdJson) => FileDirectory.fromJson(fdJson)).toList(); + return fds; + } else { + await bind.sessionReadRemoteEmptyDirsRecursiveSync( + sessionId: sessionId, path: path, includeHidden: showHidden); + return registerReadEmptyDirsTask(isLocal, path); + } + } catch (e) { + return Future.error(e); + } + } + Future fetchDirectory( String path, bool isLocal, bool showHidden) async { try { @@ -1373,6 +1478,24 @@ class PathUtil { static final windowsContext = path.Context(style: path.Style.windows); static final posixContext = path.Context(style: path.Style.posix); + static String getOtherSidePath(String mainRootPath, String mainPath, + bool isMainWindows, String otherRootPath, bool isOtherWindows) { + final mainPathUtil = isMainWindows ? windowsContext : posixContext; + final relativePath = mainPathUtil.relative(mainPath, from: mainRootPath); + + final names = mainPathUtil.split(relativePath); + + final otherPathUtil = isOtherWindows ? windowsContext : posixContext; + + String path = otherRootPath; + + for (var name in names) { + path = otherPathUtil.join(path, name); + } + + return path; + } + static String join(String path1, String path2, bool isWindows) { final pathUtil = isWindows ? windowsContext : posixContext; return pathUtil.join(path1, path2); diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 5446856fe5ab..d029aa3951ff 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -309,6 +309,8 @@ class FfiModel with ChangeNotifier { .receive(int.parse(evt['id'] as String), evt['text'] ?? ''); } else if (name == 'file_dir') { parent.target?.fileModel.receiveFileDir(evt); + } else if (name == 'empty_dirs') { + parent.target?.fileModel.receiveEmptyDirs(evt); } else if (name == 'job_progress') { parent.target?.fileModel.jobController.tryUpdateJobProgress(evt); } else if (name == 'job_done') { diff --git a/libs/hbb_common/protos/message.proto b/libs/hbb_common/protos/message.proto index 21f9e7aea0de..090b378a22cc 100644 --- a/libs/hbb_common/protos/message.proto +++ b/libs/hbb_common/protos/message.proto @@ -368,6 +368,16 @@ message ReadDir { bool include_hidden = 2; } +message ReadEmptyDirs { + string path = 1; + bool include_hidden = 2; +} + +message ReadEmptyDirsResponse { + string path = 1; + repeated FileDirectory empty_dirs = 2; +} + message ReadAllFiles { int32 id = 1; string path = 2; @@ -392,6 +402,7 @@ message FileAction { FileTransferCancel cancel = 8; FileTransferSendConfirmRequest send_confirm = 9; FileRename rename = 10; + ReadEmptyDirs read_empty_dirs = 11; } } @@ -404,6 +415,7 @@ message FileResponse { FileTransferError error = 3; FileTransferDone done = 4; FileTransferDigest digest = 5; + ReadEmptyDirsResponse empty_dirs = 6; } } diff --git a/libs/hbb_common/src/fs.rs b/libs/hbb_common/src/fs.rs index 3f236fd3ae38..8031516972ea 100644 --- a/libs/hbb_common/src/fs.rs +++ b/libs/hbb_common/src/fs.rs @@ -185,6 +185,51 @@ pub fn get_recursive_files(path: &str, include_hidden: bool) -> ResultType ResultType> { + let mut dirs = Vec::new(); + if path.is_dir() { + // to-do: symbol link handling, cp the link rather than the content + // to-do: file mode, for unix + let fd = read_dir(path, include_hidden)?; + if fd.entries.is_empty() { + dirs.push(fd); + } else { + for entry in fd.entries.iter() { + match entry.entry_type.enum_value() { + Ok(FileType::Dir) => { + if let Ok(mut tmp) = read_empty_dirs_recursive( + &path.join(&entry.name), + &prefix.join(&entry.name), + include_hidden, + ) { + for entry in tmp.drain(0..) { + dirs.push(entry); + } + } + } + _ => {} + } + } + } + Ok(dirs) + } else if path.is_file() { + Ok(dirs) + } else { + bail!("Not exists"); + } +} + +pub fn get_empty_dirs_recursive( + path: &str, + include_hidden: bool, +) -> ResultType> { + read_empty_dirs_recursive(&get_path(path), &get_path(""), include_hidden) +} + #[inline] pub fn is_file_exists(file_path: &str) -> bool { return Path::new(file_path).exists(); diff --git a/src/client/file_trait.rs b/src/client/file_trait.rs index 71ddfb09cf4b..88f0b14a5d63 100644 --- a/src/client/file_trait.rs +++ b/src/client/file_trait.rs @@ -43,6 +43,18 @@ pub trait FileManager: Interface { self.send(Data::CancelJob(id)); } + fn read_empty_dirs(&self, path: String, include_hidden: bool) { + let mut msg_out = Message::new(); + let mut file_action = FileAction::new(); + file_action.set_read_empty_dirs(ReadEmptyDirs { + path, + include_hidden, + ..Default::default() + }); + msg_out.set_file_action(file_action); + self.send(Data::Message(msg_out)); + } + fn read_remote_dir(&self, path: String, include_hidden: bool) { let mut msg_out = Message::new(); let mut file_action = FileAction::new(); diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index 8dfdffcfe1c6..df07331cfeac 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -1299,6 +1299,9 @@ impl Remote { } Some(message::Union::FileResponse(fr)) => { match fr.union { + Some(file_response::Union::EmptyDirs(res)) => { + self.handler.update_empty_dirs(res); + } Some(file_response::Union::Dir(fd)) => { #[cfg(windows)] let entries = fd.entries.to_vec(); diff --git a/src/common.rs b/src/common.rs index b1f97e27bdde..294ab97cc4f7 100644 --- a/src/common.rs +++ b/src/common.rs @@ -5,7 +5,7 @@ use std::{ task::Poll, }; -use serde_json::Value; +use serde_json::{json, Map, Value}; use hbb_common::{ allow_err, @@ -1051,6 +1051,11 @@ pub fn get_supported_keyboard_modes(version: i64, peer_platform: &str) -> Vec) -> String { + let fd_json = _make_fd_to_json(id, path, entries); + serde_json::to_string(&fd_json).unwrap_or("".into()) +} + +pub fn _make_fd_to_json(id: i32, path: String, entries: &Vec) -> Map { use serde_json::json; let mut fd_json = serde_json::Map::new(); fd_json.insert("id".into(), json!(id)); @@ -1066,7 +1071,33 @@ pub fn make_fd_to_json(id: i32, path: String, entries: &Vec) -> Strin entries_out.push(entry_map); } fd_json.insert("entries".into(), json!(entries_out)); - serde_json::to_string(&fd_json).unwrap_or("".into()) + fd_json +} + +pub fn make_vec_fd_to_json(fds: &[FileDirectory]) -> String { + let mut fd_jsons = vec![]; + + for fd in fds.iter() { + let fd_json = _make_fd_to_json(fd.id, fd.path.clone(), &fd.entries); + fd_jsons.push(fd_json); + } + + serde_json::to_string(&fd_jsons).unwrap_or("".into()) +} + +pub fn make_empty_dirs_response_to_json(res: &ReadEmptyDirsResponse) -> String { + let mut map: Map = serde_json::Map::new(); + map.insert("path".into(), json!(res.path)); + + let mut fd_jsons = vec![]; + + for fd in res.empty_dirs.iter() { + let fd_json = _make_fd_to_json(fd.id, fd.path.clone(), &fd.entries); + fd_jsons.push(fd_json); + } + map.insert("empty_dirs".into(), fd_jsons.into()); + + serde_json::to_string(&map).unwrap_or("".into()) } /// The function to handle the url scheme sent by the system. diff --git a/src/flutter.rs b/src/flutter.rs index f6fff4234813..fe0a77e39d36 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -726,6 +726,20 @@ impl InvokeUiSession for FlutterHandler { } } + fn update_empty_dirs(&self, res: ReadEmptyDirsResponse) { + self.push_event( + "empty_dirs", + &[ + ("is_local", "false"), + ( + "value", + &crate::common::make_empty_dirs_response_to_json(&res), + ), + ], + &[], + ); + } + // unused in flutter fn update_transfer_list(&self) {} diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 90dafccd027e..4c875be49b72 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -1,6 +1,6 @@ use crate::{ client::file_trait::FileManager, - common::make_fd_to_json, + common::{make_fd_to_json, make_vec_fd_to_json}, flutter::{ self, session_add, session_add_existed, session_start_, sessions, try_sync_peer_option, }, @@ -682,6 +682,27 @@ pub fn session_read_local_dir_sync( "".to_string() } +pub fn session_read_local_empty_dirs_recursive_sync( + _session_id: SessionID, + path: String, + include_hidden: bool, +) -> String { + if let Ok(fds) = fs::get_empty_dirs_recursive(&path, include_hidden) { + return make_vec_fd_to_json(&fds); + } + "".to_string() +} + +pub fn session_read_remote_empty_dirs_recursive_sync( + session_id: SessionID, + path: String, + include_hidden: bool, +) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + session.read_empty_dirs(path, include_hidden); + } +} + pub fn session_get_platform(session_id: SessionID, is_remote: bool) -> String { if let Some(session) = sessions::get_session_by_session_id(&session_id) { return session.get_platform(is_remote); diff --git a/src/ipc.rs b/src/ipc.rs index 81693a735587..e3bcfac9a49f 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -45,6 +45,10 @@ pub static EXIT_RECV_CLOSE: AtomicBool = AtomicBool::new(true); #[derive(Debug, Serialize, Deserialize, Clone)] #[serde(tag = "t", content = "c")] pub enum FS { + ReadEmptyDirs { + dir: String, + include_hidden: bool, + }, ReadDir { dir: String, include_hidden: bool, diff --git a/src/server/connection.rs b/src/server/connection.rs index 4bdda795f0b3..1aa7d7e8ac49 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -2149,6 +2149,9 @@ impl Connection { } } match fa.union { + Some(file_action::Union::ReadEmptyDirs(rd)) => { + self.read_empty_dirs(&rd.path, rd.include_hidden); + } Some(file_action::Union::ReadDir(rd)) => { self.read_dir(&rd.path, rd.include_hidden); } @@ -3145,6 +3148,14 @@ impl Connection { raii::AuthedConnID::check_remove_session(self.inner.id(), self.session_key()); } + fn read_empty_dirs(&mut self, dir: &str, include_hidden: bool) { + let dir = dir.to_string(); + self.send_fs(ipc::FS::ReadEmptyDirs { + dir, + include_hidden, + }); + } + fn read_dir(&mut self, dir: &str, include_hidden: bool) { let dir = dir.to_string(); self.send_fs(ipc::FS::ReadDir { diff --git a/src/ui_cm_interface.rs b/src/ui_cm_interface.rs index c34671d57a56..2ff5e086287d 100644 --- a/src/ui_cm_interface.rs +++ b/src/ui_cm_interface.rs @@ -751,6 +751,12 @@ async fn handle_fs( use hbb_common::fs::serialize_transfer_job; match fs { + ipc::FS::ReadEmptyDirs { + dir, + include_hidden, + } => { + read_empty_dirs(&dir, include_hidden, tx).await; + } ipc::FS::ReadDir { dir, include_hidden, @@ -907,6 +913,26 @@ async fn handle_fs( } } +#[cfg(not(any(target_os = "ios")))] +async fn read_empty_dirs(dir: &str, include_hidden: bool, tx: &UnboundedSender) { + let path = dir.to_owned(); + let path_clone = dir.to_owned(); + + if let Ok(Ok(fds)) = + spawn_blocking(move || fs::get_empty_dirs_recursive(&path, include_hidden)).await + { + let mut msg_out = Message::new(); + let mut file_response = FileResponse::new(); + file_response.set_empty_dirs(ReadEmptyDirsResponse { + path: path_clone, + empty_dirs: fds, + ..Default::default() + }); + msg_out.set_file_response(file_response); + send_raw(msg_out, tx); + } +} + #[cfg(not(any(target_os = "ios")))] async fn read_dir(dir: &str, include_hidden: bool, tx: &UnboundedSender) { let path = { diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 5194b12db867..176426464149 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -1555,6 +1555,7 @@ pub trait InvokeUiSession: Send + Sync + Clone + 'static + Sized + Default { #[cfg(feature = "flutter")] fn is_multi_ui_session(&self) -> bool; fn update_record_status(&self, start: bool); + fn update_empty_dirs(&self, _res: ReadEmptyDirsResponse) {} } impl Deref for Session { From 32a3bcdc4f8f8128cb98e1049bde24b90b187f09 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Sun, 24 Nov 2024 10:05:00 +0900 Subject: [PATCH 408/541] no custom engine --- .github/workflows/flutter-build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/flutter-build.yml b/.github/workflows/flutter-build.yml index 22b555764c26..593e5ce92c00 100644 --- a/.github/workflows/flutter-build.yml +++ b/.github/workflows/flutter-build.yml @@ -107,6 +107,7 @@ jobs: # https://github.com/flutter/flutter/issues/155685 - name: Replace engine with rustdesk custom flutter engine + if: false run: | flutter doctor -v flutter precache --windows From 152d0ce74b3592698cffce2bec4b733b4e85c3f7 Mon Sep 17 00:00:00 2001 From: 21pages Date: Sun, 24 Nov 2024 21:07:03 +0800 Subject: [PATCH 409/541] install vaapi dependencies before vcpkg installs ffmpeg (#10035) linux vaapi encoding/decoding lost since installing ffmpeg with vcpkg Signed-off-by: 21pages --- .github/workflows/flutter-build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/flutter-build.yml b/.github/workflows/flutter-build.yml index 593e5ce92c00..aefd06e7d536 100644 --- a/.github/workflows/flutter-build.yml +++ b/.github/workflows/flutter-build.yml @@ -1441,6 +1441,7 @@ jobs: - name: Install vcpkg dependencies if: matrix.job.arch == 'x86_64' || env.UPLOAD_ARTIFACT == 'true' run: | + sudo apt install -y libva-dev libvdpau-dev if ! $VCPKG_ROOT/vcpkg \ install \ --triplet ${{ matrix.job.vcpkg-triplet }} \ From 34b93c6f8369e06ad64f94ebbfc1dfc6255fd943 Mon Sep 17 00:00:00 2001 From: 21pages Date: Mon, 25 Nov 2024 11:09:17 +0800 Subject: [PATCH 410/541] fix aom pts (#10042) the old pts/duration is wrong, use timebase 1/1000 like other codecs, not found any differences. Signed-off-by: 21pages --- libs/scrap/src/common/aom.rs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/libs/scrap/src/common/aom.rs b/libs/scrap/src/common/aom.rs index 00d6fe506813..d2bb2feb77f2 100644 --- a/libs/scrap/src/common/aom.rs +++ b/libs/scrap/src/common/aom.rs @@ -66,7 +66,7 @@ mod webrtc { const kMaxQindex: u32 = 205; // Max qindex threshold for QP scaling. const kBitDepth: u32 = 8; const kLagInFrames: u32 = 0; // No look ahead. - const kRtpTicksPerSecond: i32 = 90000; + pub(super) const kTimeBaseDen: i64 = 1000; const kMinimumFrameRate: f64 = 1.0; pub const DEFAULT_Q_MAX: u32 = 56; // no more than 63 @@ -108,7 +108,7 @@ mod webrtc { c.g_h = cfg.height; c.g_threads = codec_thread_num(64) as _; c.g_timebase.num = 1; - c.g_timebase.den = kRtpTicksPerSecond; + c.g_timebase.den = kTimeBaseDen as _; c.g_input_bit_depth = kBitDepth; if let Some(keyframe_interval) = cfg.keyframe_interval { c.kf_min_dist = 0; @@ -313,7 +313,7 @@ impl EncoderApi for AomEncoder { } impl AomEncoder { - pub fn encode(&mut self, pts: i64, data: &[u8], stride_align: usize) -> Result { + pub fn encode(&mut self, ms: i64, data: &[u8], stride_align: usize) -> Result { let bpp = if self.i444 { 24 } else { 12 }; if data.len() < self.width * self.height * bpp / 8 { return Err(Error::FailedCall("len not enough".to_string())); @@ -333,13 +333,14 @@ impl AomEncoder { stride_align as _, data.as_ptr() as _, )); - + let pts = webrtc::kTimeBaseDen / 1000 * ms; + let duration = webrtc::kTimeBaseDen / 1000; call_aom!(aom_codec_encode( &mut self.ctx, &image, pts as _, - 1, // Duration - 0, // Flags + duration as _, // Duration + 0, // Flags )); Ok(EncodeFrames { From 9d2bdfefb18afb3c92d4719e08e3d242739e7dd3 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Mon, 25 Nov 2024 11:12:43 +0800 Subject: [PATCH 411/541] feat: update build_fdroid.sh (#10040) Signed-off-by: fufesou --- flutter/build_fdroid.sh | 110 ++++++++++++++++++++-------------------- 1 file changed, 56 insertions(+), 54 deletions(-) diff --git a/flutter/build_fdroid.sh b/flutter/build_fdroid.sh index 7f3a9cc48f4a..e0a4eb5991b9 100755 --- a/flutter/build_fdroid.sh +++ b/flutter/build_fdroid.sh @@ -82,6 +82,21 @@ export PATH="${PATH}:${HOME}/flutter/bin:${HOME}/depot_tools" export VCPKG_ROOT="${HOME}/vcpkg" +prepare_Flutter() { + version="${1}" + + pushd "${HOME}" + if [ ! -f "${HOME}/flutter/bin/flutter" ]; then + git clone https://github.com/flutter/flutter + fi + pushd flutter + git restore . + git checkout "${version}" + flutter config --no-analytics + popd # flutter + popd # ${HOME} +} + # Now act depending on build step # NOTE: F-Droid maintainers require explicit declaration of dependencies @@ -110,9 +125,13 @@ prebuild) .github/workflows/flutter-build.yml)" fi + FLUTTER_BRIDGE_VERSION="$(yq -r \ + .env.FLUTTER_VERSION \ + .github/workflows/bridge.yml)" + FLUTTER_RUST_BRIDGE_VERSION="$(yq -r \ .env.FLUTTER_RUST_BRIDGE_VERSION \ - .github/workflows/flutter-build.yml)" + .github/workflows/bridge.yml)" NDK_VERSION="$(yq -r \ .env.NDK_VERSION \ @@ -127,6 +146,7 @@ prebuild) .github/workflows/flutter-build.yml)" if [ -z "${CARGO_NDK_VERSION}" ] || [ -z "${FLUTTER_VERSION}" ] || + [ -z "${FLUTTER_BRIDGE_VERSION}" ] || [ -z "${FLUTTER_RUST_BRIDGE_VERSION}" ] || [ -z "${NDK_VERSION}" ] || [ -z "${RUST_VERSION}" ] || [ -z "${VCPKG_COMMIT_ID}" ]; then @@ -163,24 +183,6 @@ prebuild) 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 @@ -275,12 +277,46 @@ prebuild) git apply res/fdroid/patches/*.patch + # Backup .gclient file, for later restore + + cp flutter-sdk/.gclient flutter-sdk/.gclient.bak + + # For FLUTTER_BRIDGE_VERSION + sed \ + -i \ + -e 's/extended_text: 14.0.0/extended_text: 13.0.0/g' \ + flutter/pubspec.yaml + + # Install Flutter bridge version + prepare_Flutter "${FLUTTER_BRIDGE_VERSION}" + cp flutter-sdk/.gclient.bak flutter-sdk/.gclient + sed -i "s/FLUTTER_VERSION_PLACEHOLDER/${FLUTTER_BRIDGE_VERSION}/" flutter-sdk/.gclient + + # Download Flutter dependencies + pushd flutter + flutter clean && 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 + + git restore flutter/pubspec.* + + # Install Flutter + prepare_Flutter "${FLUTTER_VERSION}" + cp flutter-sdk/.gclient.bak flutter-sdk/.gclient + sed -i "s/FLUTTER_VERSION_PLACEHOLDER/${FLUTTER_VERSION}/" flutter-sdk/.gclient + + # gms is not in thoes files now, but we still keep the following line for future reference(maybe). sed \ -i \ -e '/gms/d' \ flutter/android/build.gradle \ flutter/android/app/build.gradle + # `firebase_analytics`` is not in thoes files now, but we still keep the following lines. sed \ -i \ -e '/firebase_analytics/d' \ @@ -296,34 +332,6 @@ prebuild) -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 @@ -373,16 +381,10 @@ build) pushd flutter - flutter packages pub get + flutter clean && 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}" From 30a11bfe0a4166d48e8d39a25311641c8cc166d9 Mon Sep 17 00:00:00 2001 From: 21pages Date: Mon, 25 Nov 2024 21:45:58 +0800 Subject: [PATCH 412/541] android wait 3s for isStart flag (#10053) The normal process is that `startCapture` and `VideoService::run` run in parallel, the `run` function waits for startCapture to complete, then sets the scale, and subsequently calls `stopCapture` and `startCapture`. If the `run` function does not wait long enough, `startCapture` initializes the surface with the original width and height, but the `start` flag is still false, meaning it can't call `stopCapture` and `startCapture`. This results in only capturing the upper-left portion of the virtual display. Signed-off-by: 21pages --- src/server/video_service.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/server/video_service.rs b/src/server/video_service.rs index eceaf69c1a6c..733405a37f70 100644 --- a/src/server/video_service.rs +++ b/src/server/video_service.rs @@ -845,14 +845,15 @@ fn check_change_scale(hardware: bool) -> ResultType<()> { use hbb_common::config::keys::OPTION_ENABLE_ANDROID_SOFTWARE_ENCODING_HALF_SCALE as SCALE_SOFT; // isStart flag is set at the end of startCapture() in Android, wait it to be set. - for i in 0..6 { + let n = 60; // 3s + for i in 0..n { if scrap::is_start() == Some(true) { log::info!("start flag is set"); break; } log::info!("wait for start, {i}"); std::thread::sleep(Duration::from_millis(50)); - if i == 5 { + if i == n - 1 { log::error!("wait for start timeout"); } } From d61c99b1051d14bf88056ca5099726aad9c90fdc Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Mon, 25 Nov 2024 21:49:09 +0800 Subject: [PATCH 413/541] fix: Android 29, crash on restart and reconn (#10054) Signed-off-by: fufesou --- .../com/carriez/flutter_hbb/MainActivity.kt | 18 ----------- flutter/android/app/src/main/kotlin/ffi.kt | 1 - libs/scrap/src/android/ffi.rs | 30 ++++++------------- 3 files changed, 9 insertions(+), 40 deletions(-) diff --git a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainActivity.kt b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainActivity.kt index 15dee6002f57..333ea9f14bd2 100644 --- a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainActivity.kt +++ b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainActivity.kt @@ -32,10 +32,6 @@ import com.hjq.permissions.XXPermissions import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.engine.FlutterEngine import io.flutter.plugin.common.MethodChannel -import io.flutter.embedding.engine.plugins.FlutterPlugin -import io.flutter.plugin.common.MethodCall -import io.flutter.plugin.common.MethodChannel.MethodCallHandler -import io.flutter.plugin.common.MethodChannel.Result import kotlin.concurrent.thread @@ -66,7 +62,6 @@ class MainActivity : FlutterActivity() { channelTag ) initFlutterChannel(flutterMethodChannel!!) - flutterEngine.plugins.add(ContextPlugin()) thread { setCodecInfo() } } @@ -417,16 +412,3 @@ class MainActivity : FlutterActivity() { } } } - -// https://cjycode.com/flutter_rust_bridge/guides/how-to/ndk-init -class ContextPlugin : FlutterPlugin, MethodCallHandler { - override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { - FFI.initContext(flutterPluginBinding.applicationContext) - } - override fun onMethodCall(call: MethodCall, result: Result) { - result.notImplemented() - } - - override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { - } -} \ No newline at end of file diff --git a/flutter/android/app/src/main/kotlin/ffi.kt b/flutter/android/app/src/main/kotlin/ffi.kt index 653465782b3c..69b395ac2e9d 100644 --- a/flutter/android/app/src/main/kotlin/ffi.kt +++ b/flutter/android/app/src/main/kotlin/ffi.kt @@ -13,7 +13,6 @@ object FFI { } external fun init(ctx: Context) - external fun initContext(ctx: Context) external fun setClipboardManager(clipboardManager: RdClipboardManager) external fun startServer(app_dir: String, custom_client_config: String) external fun startService() diff --git a/libs/scrap/src/android/ffi.rs b/libs/scrap/src/android/ffi.rs index 0e48f60e6f72..ffa38a965d73 100644 --- a/libs/scrap/src/android/ffi.rs +++ b/libs/scrap/src/android/ffi.rs @@ -205,18 +205,6 @@ pub extern "system" fn Java_ffi_FFI_init(env: JNIEnv, _class: JClass, ctx: JObje } } -#[no_mangle] -pub extern "system" fn Java_ffi_FFI_initContext(env: JNIEnv, _class: JClass, ctx: JObject) { - log::debug!("MainActivity initContext from java"); - if let Ok(jvm) = env.get_java_vm() { - if let Ok(context) = env.new_global_ref(ctx) { - let java_vm = jvm.get_java_vm_pointer() as *mut c_void; - let context_jobject = context.as_obj().as_raw() as *mut c_void; - init_ndk_context(java_vm, context_jobject); - } - } -} - #[no_mangle] pub extern "system" fn Java_ffi_FFI_setClipboardManager( env: JNIEnv, @@ -482,12 +470,12 @@ fn init_ndk_context(java_vm: *mut c_void, context_jobject: *mut c_void) { *lock = true; } -// // https://cjycode.com/flutter_rust_bridge/guides/how-to/ndk-init -// #[no_mangle] -// pub extern "C" fn JNI_OnLoad(vm: jni::JavaVM, res: *mut std::os::raw::c_void) -> jni::sys::jint { -// if let Ok(env) = vm.get_env() { -// let vm = vm.get_java_vm_pointer() as *mut std::os::raw::c_void; -// init_ndk_context(vm, res); -// } -// jni::JNIVersion::V6.into() -// } +// https://cjycode.com/flutter_rust_bridge/guides/how-to/ndk-init +#[no_mangle] +pub extern "C" fn JNI_OnLoad(vm: jni::JavaVM, res: *mut std::os::raw::c_void) -> jni::sys::jint { + if let Ok(env) = vm.get_env() { + let vm = vm.get_java_vm_pointer() as *mut std::os::raw::c_void; + init_ndk_context(vm, res); + } + jni::JNIVersion::V6.into() +} From edfae98a01db9cec0499fd94e34e867548f0ce91 Mon Sep 17 00:00:00 2001 From: flusheDData <116861809+flusheDData@users.noreply.github.com> Date: Mon, 25 Nov 2024 16:46:28 +0100 Subject: [PATCH 414/541] Update es.rs (#10055) New term added --- src/lang/es.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lang/es.rs b/src/lang/es.rs index 7f3bf0f70f66..ee0ffe569942 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -653,6 +653,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", "Subir carpeta"), ("Upload files", "Subir archivos"), ("Clipboard is synchronized", "Portapapeles sincronizado"), - ("Update client clipboard", ""), + ("Update client clipboard", "Actualizar portapapeles del cliente"), ].iter().cloned().collect(); } From 8a70932cd663971bb1ec32696f5a27025a3e7563 Mon Sep 17 00:00:00 2001 From: Yevhen Popok Date: Tue, 26 Nov 2024 03:51:27 +0200 Subject: [PATCH 415/541] Update Ukrainian UI translation (#10056) --- src/lang/uk.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/lang/uk.rs b/src/lang/uk.rs index 3ef8f4c6fdde..ad83430505e6 100644 --- a/src/lang/uk.rs +++ b/src/lang/uk.rs @@ -147,7 +147,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("OS Password", "Пароль ОС"), ("install_tip", "Через UAC, в деяких випадках RustDesk може працювати некоректно на віддаленому вузлі. Щоб уникнути UAC, натисніть кнопку нижче для встановлення RustDesk в системі"), ("Click to upgrade", "Натисніть, щоб перевірити наявність оновлень"), - ("Click to download", "Натисніть, щоб завантажити"), + ("Click to download", "Натисніть, щоб отримати"), ("Click to update", "Натисніть, щоб оновити"), ("Configure", "Налаштувати"), ("config_acc", "Для віддаленого керування вашою стільницею, вам необхідно надати RustDesk дозволи \"Спеціальні можливості\""), @@ -246,7 +246,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Paste", "Вставити"), ("Paste here?", "Вставити сюди?"), ("Are you sure to close the connection?", "Ви впевнені, що хочете завершити підключення?"), - ("Download new version", "Завантажте нову версію"), + ("Download new version", "Отримайте нову версію"), ("Touch mode", "Сенсорний режим"), ("Mouse mode", "Режим миші"), ("One-Finger Tap", "Дотик одним пальцем"), @@ -307,7 +307,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Ignore Battery Optimizations", "Ігнорувати оптимізації батареї"), ("android_open_battery_optimizations_tip", "Перейдіть на наступну сторінку налаштувань"), ("Start on boot", "Автозапуск"), - ("Start the screen sharing service on boot, requires special permissions", "Запустити службу службу спільного доступу до екрана під час завантаження, потребує спеціальних дозволів"), + ("Start the screen sharing service on boot, requires special permissions", "Запускати службу спільного доступу до екрана під час завантаження, потребує спеціальних дозволів"), ("Connection not allowed", "Підключення не дозволено"), ("Legacy mode", "Застарілий режим"), ("Map mode", "Режим карти"), @@ -648,11 +648,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("one-way-file-transfer-tip", "На стороні, що керується, увімкнено односторонню передачу файлів."), ("Authentication Required", "Потрібна автентифікація"), ("Authenticate", "Автентифікувати"), - ("web_id_input_tip", "Ви можете ввести ID з того самого серверу, прямий IP-доступ у веб-клієнті не підтримується.\nЯкщо ви хочете отримати доступ до пристрою на іншому сервері, будь ласка, додайте адресу сервера (@<адреса_сервера>?key=<значення_ключа>), наприклад,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nЯкщо ви хочете отримати доступ до пристрою на публічному сервері, будь ласка, введіть \"@public\", для публічного сервера ключ не потрібен."), - ("Download", ""), - ("Upload folder", ""), - ("Upload files", ""), - ("Clipboard is synchronized", ""), - ("Update client clipboard", ""), + ("web_id_input_tip", "Ви можете ввести ID на тому самому серверу, прямий IP-доступ у веб-клієнті не підтримується.\nЯкщо ви хочете отримати доступ до пристрою на іншому сервері, будь ласка, додайте адресу сервера (@<адреса_сервера>?key=<значення_ключа>). Наприклад,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nЯкщо ви хочете отримати доступ до пристрою на публічному сервері, будь ласка, введіть \"@public\". Для публічного сервера ключ не потрібен."), + ("Download", "Отримати"), + ("Upload folder", "Надіслати теку"), + ("Upload files", "Надіслати файли"), + ("Clipboard is synchronized", "Буфер обміну синхронізовано"), + ("Update client clipboard", "Оновити буфер обміну клієнта"), ].iter().cloned().collect(); } From 458a88fb894c3d32a371fa9a129c2288969a476f Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Tue, 26 Nov 2024 16:24:51 +0800 Subject: [PATCH 416/541] fix: mobile autocomplete options (#10060) Signed-off-by: fufesou --- flutter/lib/mobile/pages/connection_page.dart | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/flutter/lib/mobile/pages/connection_page.dart b/flutter/lib/mobile/pages/connection_page.dart index 89b71c177c91..de68aa510b67 100644 --- a/flutter/lib/mobile/pages/connection_page.dart +++ b/flutter/lib/mobile/pages/connection_page.dart @@ -48,6 +48,9 @@ class _ConnectionPageState extends State { bool isPeersLoaded = false; StreamSubscription? _uniLinksSubscription; + // https://github.com/flutter/flutter/issues/157244 + Iterable _autocompleteOpts = []; + _ConnectionPageState() { if (!isWeb) _uniLinksSubscription = listenUniLinks(); _idController.addListener(() { @@ -166,7 +169,7 @@ class _ConnectionPageState extends State { child: Autocomplete( optionsBuilder: (TextEditingValue textEditingValue) { if (textEditingValue.text == '') { - return const Iterable.empty(); + _autocompleteOpts = const Iterable.empty(); } else if (peers.isEmpty && !isPeersLoaded) { Peer emptyPeer = Peer( id: '', @@ -182,7 +185,7 @@ class _ConnectionPageState extends State { rdpUsername: '', loginName: '', ); - return [emptyPeer]; + _autocompleteOpts = [emptyPeer]; } else { String textWithoutSpaces = textEditingValue.text.replaceAll(" ", ""); @@ -194,7 +197,7 @@ class _ConnectionPageState extends State { } String textToFind = textEditingValue.text.toLowerCase(); - return peers + _autocompleteOpts = peers .where((peer) => peer.id.toLowerCase().contains(textToFind) || peer.username @@ -206,6 +209,7 @@ class _ConnectionPageState extends State { peer.alias.toLowerCase().contains(textToFind)) .toList(); } + return _autocompleteOpts; }, fieldViewBuilder: (BuildContext context, TextEditingController fieldTextEditingController, @@ -274,6 +278,7 @@ class _ConnectionPageState extends State { optionsViewBuilder: (BuildContext context, AutocompleteOnSelected onSelected, Iterable options) { + options = _autocompleteOpts; double maxHeight = options.length * 50; if (options.length == 1) { maxHeight = 52; From 84dab0e96f3a30ad0d17f062db35b157389823de Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Tue, 26 Nov 2024 20:33:54 +0800 Subject: [PATCH 417/541] Fix/android keyboard map mode workaround (#10064) * fix: Android, keyboard, map mode, workaround The `KeyEvent.physicalKey.usbHidUsage` are wrong if using Microsoft SwiftKey keyboard. `window.addFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM)` is a workaround for this issue. Signed-off-by: fufesou * fix: clear callback on first image Signed-off-by: fufesou * Android disable soft keyboard in remote page if not editing. Signed-off-by: fufesou --------- Signed-off-by: fufesou --- flutter/lib/mobile/pages/remote_page.dart | 27 ++++++++++++++++++++--- flutter/lib/models/model.dart | 1 + 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index 1dee69b94ee7..4457cbe26a26 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -26,6 +26,19 @@ import '../widgets/dialog.dart'; final initText = '1' * 1024; +// Workaround for Android (default input method, Microsoft SwiftKey keyboard) when using physical keyboard. +// When connecting a physical keyboard, `KeyEvent.physicalKey.usbHidUsage` are wrong is using Microsoft SwiftKey keyboard. +// https://github.com/flutter/flutter/issues/159384 +// https://github.com/flutter/flutter/issues/159383 +void _disableAndroidSoftKeyboard({bool? isKeyboardVisible}) { + if (isAndroid) { + if (isKeyboardVisible != true) { + // `enable_soft_keyboard` will be set to `true` when clicking the keyboard icon, in `openKeyboard()`. + gFFI.invokeMethod("enable_soft_keyboard", false); + } + } +} + class RemotePage extends StatefulWidget { RemotePage({Key? key, required this.id, this.password, this.isSharedPassword}) : super(key: key); @@ -99,6 +112,8 @@ class _RemotePageState extends State with WidgetsBindingObserver { if (gFFI.recordingModel.start) { showToast(translate('Automatically record outgoing sessions')); } + _disableAndroidSoftKeyboard( + isKeyboardVisible: keyboardVisibilityController.isVisible); }); WidgetsBinding.instance.addObserver(this); } @@ -1244,7 +1259,9 @@ void showOptions( toggles + [privacyModeWidget]), ); - }, clickMaskDismiss: true, backDismiss: true); + }, clickMaskDismiss: true, backDismiss: true).then((value) { + _disableAndroidSoftKeyboard(); + }); } TTextMenu? getVirtualDisplayMenu(FFI ffi, String id) { @@ -1263,7 +1280,9 @@ TTextMenu? getVirtualDisplayMenu(FFI ffi, String id) { children: children, ), ); - }, clickMaskDismiss: true, backDismiss: true); + }, clickMaskDismiss: true, backDismiss: true).then((value) { + _disableAndroidSoftKeyboard(); + }); }, ); } @@ -1305,7 +1324,9 @@ TTextMenu? getResolutionMenu(FFI ffi, String id) { children: children, ), ); - }, clickMaskDismiss: true, backDismiss: true); + }, clickMaskDismiss: true, backDismiss: true).then((value) { + _disableAndroidSoftKeyboard(); + }); }, ); } diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index d029aa3951ff..2c04eae6c87f 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -2859,6 +2859,7 @@ class FFI { canvasModel.scale, ffiModel.pi.currentDisplay); } + imageModel.callbacksOnFirstImage.clear(); await imageModel.update(null); cursorModel.clear(); ffiModel.clear(); From b99c540210a98df39b96ec65f2ec40fd703c7506 Mon Sep 17 00:00:00 2001 From: 21pages Date: Tue, 26 Nov 2024 20:35:17 +0800 Subject: [PATCH 418/541] add "Untagged" to filter addressbook peers without tags (#10063) Signed-off-by: 21pages --- flutter/lib/common/widgets/address_book.dart | 45 +++++++++++++------- flutter/lib/common/widgets/peers_view.dart | 12 +++++- flutter/lib/models/ab_model.dart | 3 ++ src/lang/ar.rs | 1 + src/lang/be.rs | 1 + src/lang/bg.rs | 1 + src/lang/ca.rs | 1 + src/lang/cn.rs | 1 + src/lang/cs.rs | 1 + src/lang/da.rs | 1 + src/lang/de.rs | 1 + src/lang/el.rs | 1 + src/lang/eo.rs | 1 + src/lang/es.rs | 1 + src/lang/et.rs | 1 + src/lang/eu.rs | 1 + src/lang/fa.rs | 1 + src/lang/fr.rs | 1 + src/lang/he.rs | 1 + src/lang/hr.rs | 1 + src/lang/hu.rs | 1 + src/lang/id.rs | 1 + src/lang/it.rs | 1 + src/lang/ja.rs | 1 + src/lang/ko.rs | 1 + src/lang/kz.rs | 1 + src/lang/lt.rs | 1 + src/lang/lv.rs | 1 + src/lang/nb.rs | 1 + src/lang/nl.rs | 1 + src/lang/pl.rs | 1 + src/lang/pt_PT.rs | 1 + src/lang/ptbr.rs | 1 + src/lang/ro.rs | 1 + src/lang/ru.rs | 1 + src/lang/sk.rs | 1 + src/lang/sl.rs | 1 + src/lang/sq.rs | 1 + src/lang/sr.rs | 1 + src/lang/sv.rs | 1 + src/lang/template.rs | 1 + src/lang/th.rs | 1 + src/lang/tr.rs | 1 + src/lang/tw.rs | 1 + src/lang/uk.rs | 1 + src/lang/vn.rs | 1 + 46 files changed, 85 insertions(+), 18 deletions(-) diff --git a/flutter/lib/common/widgets/address_book.dart b/flutter/lib/common/widgets/address_book.dart index 78bd20ef0336..ae07c1498cf1 100644 --- a/flutter/lib/common/widgets/address_book.dart +++ b/flutter/lib/common/widgets/address_book.dart @@ -1,5 +1,6 @@ import 'dart:math'; +import 'package:bot_toast/bot_toast.dart'; import 'package:dropdown_button2/dropdown_button2.dart'; import 'package:dynamic_layouts/dynamic_layouts.dart'; import 'package:flutter/material.dart'; @@ -316,13 +317,14 @@ class _AddressBookState extends State { Widget _buildTags() { return Obx(() { - final List tags; + List tags; if (gFFI.abModel.sortTags.value) { tags = gFFI.abModel.currentAbTags.toList(); tags.sort(); } else { - tags = gFFI.abModel.currentAbTags; + tags = gFFI.abModel.currentAbTags.toList(); } + tags = [kUntagged, ...tags].toList(); final editPermission = gFFI.abModel.current.canWrite(); tagBuilder(String e) { return AddressBookTag( @@ -669,6 +671,14 @@ class _AddressBookState extends State { } else { final tags = field.trim().split(RegExp(r"[\s,;\n]+")); field = tags.join(','); + for (var t in [kUntagged, translate(kUntagged)]) { + if (tags.contains(t)) { + BotToast.showText( + contentColor: Colors.red, text: 'Tag name cannot be "$t"'); + isInProgress = false; + return; + } + } gFFI.abModel.addTags(tags); // final currentPeers } @@ -741,12 +751,14 @@ class AddressBookTag extends StatelessWidget { } const double radius = 8; + final isUnTagged = name == kUntagged; + final showAction = showActionMenu && !isUnTagged; return GestureDetector( onTap: onTap, - onTapDown: showActionMenu ? setPosition : null, - onSecondaryTapDown: showActionMenu ? setPosition : null, - onSecondaryTap: showActionMenu ? () => _showMenu(context, pos) : null, - onLongPress: showActionMenu ? () => _showMenu(context, pos) : null, + onTapDown: showAction ? setPosition : null, + onSecondaryTapDown: showAction ? setPosition : null, + onSecondaryTap: showAction ? () => _showMenu(context, pos) : null, + onLongPress: showAction ? () => _showMenu(context, pos) : null, child: Obx(() => Container( decoration: BoxDecoration( color: tags.contains(name) @@ -758,17 +770,18 @@ class AddressBookTag extends StatelessWidget { child: IntrinsicWidth( child: Row( children: [ - Container( - width: radius, - height: radius, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: tags.contains(name) - ? Colors.white - : gFFI.abModel.getCurrentAbTagColor(name)), - ).marginOnly(right: radius / 2), + if (!isUnTagged) + Container( + width: radius, + height: radius, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: tags.contains(name) + ? Colors.white + : gFFI.abModel.getCurrentAbTagColor(name)), + ).marginOnly(right: radius / 2), Expanded( - child: Text(name, + child: Text(isUnTagged ? translate(name) : name, style: TextStyle( overflow: TextOverflow.ellipsis, color: tags.contains(name) ? Colors.white : null)), diff --git a/flutter/lib/common/widgets/peers_view.dart b/flutter/lib/common/widgets/peers_view.dart index 7f16850219f4..e14e198bd105 100644 --- a/flutter/lib/common/widgets/peers_view.dart +++ b/flutter/lib/common/widgets/peers_view.dart @@ -6,6 +6,7 @@ import 'package:flutter/foundation.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/models/ab_model.dart'; import 'package:flutter_hbb/models/peer_tab_model.dart'; import 'package:flutter_hbb/models/state_model.dart'; import 'package:get/get.dart'; @@ -532,15 +533,22 @@ class AddressBookPeersView extends BasePeersView { if (selectedTags.isEmpty) { return true; } + // The result of a no-tag union with normal tags, still allows normal tags to perform union or intersection operations. + final selectedNormalTags = + selectedTags.where((tag) => tag != kUntagged).toList(); + if (selectedTags.contains(kUntagged)) { + if (idents.isEmpty) return true; + if (selectedNormalTags.isEmpty) return false; + } if (gFFI.abModel.filterByIntersection.value) { - for (final tag in selectedTags) { + for (final tag in selectedNormalTags) { if (!idents.contains(tag)) { return false; } } return true; } else { - for (final tag in selectedTags) { + for (final tag in selectedNormalTags) { if (idents.contains(tag)) { return true; } diff --git a/flutter/lib/models/ab_model.dart b/flutter/lib/models/ab_model.dart index 0da84e0f26c8..613ec1ed350d 100644 --- a/flutter/lib/models/ab_model.dart +++ b/flutter/lib/models/ab_model.dart @@ -33,6 +33,8 @@ bool filterAbTagByIntersection() { const _personalAddressBookName = "My address book"; const _legacyAddressBookName = "Legacy address book"; +const kUntagged = "Untagged"; + enum ForcePullAb { listAndCurrent, current, @@ -424,6 +426,7 @@ class AbModel { // #region tags Future addTags(List tagList) async { + tagList.removeWhere((e) => e == kUntagged); final ret = await current.addTags(tagList, {}); await pullNonLegacyAfterChange(); _saveCache(); diff --git a/src/lang/ar.rs b/src/lang/ar.rs index 039ad4b114ec..1b20ebc4a99a 100644 --- a/src/lang/ar.rs +++ b/src/lang/ar.rs @@ -654,5 +654,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload files", ""), ("Clipboard is synchronized", ""), ("Update client clipboard", ""), + ("Untagged", ""), ].iter().cloned().collect(); } diff --git a/src/lang/be.rs b/src/lang/be.rs index 26281c26b4aa..7c081983782a 100644 --- a/src/lang/be.rs +++ b/src/lang/be.rs @@ -654,5 +654,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload files", ""), ("Clipboard is synchronized", ""), ("Update client clipboard", ""), + ("Untagged", ""), ].iter().cloned().collect(); } diff --git a/src/lang/bg.rs b/src/lang/bg.rs index 46126056c9be..3c1d202eeb41 100644 --- a/src/lang/bg.rs +++ b/src/lang/bg.rs @@ -654,5 +654,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload files", ""), ("Clipboard is synchronized", ""), ("Update client clipboard", ""), + ("Untagged", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ca.rs b/src/lang/ca.rs index d680b66a5e87..120200b3520c 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -654,5 +654,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload files", ""), ("Clipboard is synchronized", ""), ("Update client clipboard", ""), + ("Untagged", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index d15c1b6ba0e1..901b4cdb90ff 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -654,5 +654,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload files", "上传文件"), ("Clipboard is synchronized", "剪贴板已同步"), ("Update client clipboard", "更新客户端的粘贴板"), + ("Untagged", "无标签"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index c4ff80c7e3ff..25046cbcb40e 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -654,5 +654,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload files", ""), ("Clipboard is synchronized", ""), ("Update client clipboard", ""), + ("Untagged", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index 905f31739480..fa3e6f100697 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -654,5 +654,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload files", ""), ("Clipboard is synchronized", ""), ("Update client clipboard", ""), + ("Untagged", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index ba91471c92d4..482b45bfc79b 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -654,5 +654,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload files", "Dateien hochladen"), ("Clipboard is synchronized", "Zwischenablage ist synchronisiert"), ("Update client clipboard", "Client-Zwischenablage aktualisieren"), + ("Untagged", ""), ].iter().cloned().collect(); } diff --git a/src/lang/el.rs b/src/lang/el.rs index 73a306c7c9aa..57984b7288f0 100644 --- a/src/lang/el.rs +++ b/src/lang/el.rs @@ -654,5 +654,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload files", ""), ("Clipboard is synchronized", ""), ("Update client clipboard", ""), + ("Untagged", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 83747f03cd48..5fe2c8d3a65b 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -654,5 +654,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload files", ""), ("Clipboard is synchronized", ""), ("Update client clipboard", ""), + ("Untagged", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index ee0ffe569942..4b84b1c0f46a 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -654,5 +654,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload files", "Subir archivos"), ("Clipboard is synchronized", "Portapapeles sincronizado"), ("Update client clipboard", "Actualizar portapapeles del cliente"), + ("Untagged", ""), ].iter().cloned().collect(); } diff --git a/src/lang/et.rs b/src/lang/et.rs index 9f67c12262d7..931a3da2d3e5 100644 --- a/src/lang/et.rs +++ b/src/lang/et.rs @@ -654,5 +654,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload files", ""), ("Clipboard is synchronized", ""), ("Update client clipboard", ""), + ("Untagged", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eu.rs b/src/lang/eu.rs index 93f5a60b4ef6..e191a74f09c9 100644 --- a/src/lang/eu.rs +++ b/src/lang/eu.rs @@ -654,5 +654,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload files", ""), ("Clipboard is synchronized", ""), ("Update client clipboard", ""), + ("Untagged", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index 33c6b7427c1a..051859f60eab 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -654,5 +654,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload files", ""), ("Clipboard is synchronized", ""), ("Update client clipboard", ""), + ("Untagged", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 8c8332ad1edc..9366c6a19309 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -654,5 +654,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload files", ""), ("Clipboard is synchronized", ""), ("Update client clipboard", ""), + ("Untagged", ""), ].iter().cloned().collect(); } diff --git a/src/lang/he.rs b/src/lang/he.rs index 400b5156b40e..39cd98fc4c10 100644 --- a/src/lang/he.rs +++ b/src/lang/he.rs @@ -654,5 +654,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload files", ""), ("Clipboard is synchronized", ""), ("Update client clipboard", ""), + ("Untagged", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hr.rs b/src/lang/hr.rs index d6389480a04d..d3163ae03c7e 100644 --- a/src/lang/hr.rs +++ b/src/lang/hr.rs @@ -654,5 +654,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload files", ""), ("Clipboard is synchronized", ""), ("Update client clipboard", ""), + ("Untagged", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index fc58fe5a6949..c348db1d1d60 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -654,5 +654,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload files", "Fájlok feltöltése"), ("Clipboard is synchronized", "A vágólap szinkronizálva van"), ("Update client clipboard", ""), + ("Untagged", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index b488f5740d9e..d52c11a384e0 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -654,5 +654,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload files", ""), ("Clipboard is synchronized", ""), ("Update client clipboard", ""), + ("Untagged", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index b5a191f093ef..5bec55f4f62e 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -654,5 +654,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload files", "File upload"), ("Clipboard is synchronized", "Gli appunti sono sincronizzati"), ("Update client clipboard", "Aggiorna appunti client"), + ("Untagged", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 1d0f3b7ea157..14c06e0d59bc 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -654,5 +654,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload files", ""), ("Clipboard is synchronized", ""), ("Update client clipboard", ""), + ("Untagged", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index f266f2536895..527813d09eba 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -654,5 +654,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload files", "파일 업로드"), ("Clipboard is synchronized", "클립보드가 동기화됨"), ("Update client clipboard", ""), + ("Untagged", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index 46733ce713e5..f9ee96ca2518 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -654,5 +654,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload files", ""), ("Clipboard is synchronized", ""), ("Update client clipboard", ""), + ("Untagged", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lt.rs b/src/lang/lt.rs index 723b46a30a81..31522364c1b5 100644 --- a/src/lang/lt.rs +++ b/src/lang/lt.rs @@ -654,5 +654,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload files", ""), ("Clipboard is synchronized", ""), ("Update client clipboard", ""), + ("Untagged", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lv.rs b/src/lang/lv.rs index 0439a45e65f5..166830c16b4b 100644 --- a/src/lang/lv.rs +++ b/src/lang/lv.rs @@ -654,5 +654,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload files", "Augšupielādēt failus"), ("Clipboard is synchronized", "Starpliktuve ir sinhronizēta"), ("Update client clipboard", ""), + ("Untagged", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nb.rs b/src/lang/nb.rs index c9f3ce243921..00ee513b14c0 100644 --- a/src/lang/nb.rs +++ b/src/lang/nb.rs @@ -654,5 +654,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload files", ""), ("Clipboard is synchronized", ""), ("Update client clipboard", ""), + ("Untagged", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nl.rs b/src/lang/nl.rs index 78c6f77b753a..f1c7ba97f9b1 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -654,5 +654,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload files", "Bestanden uploaden"), ("Clipboard is synchronized", "Klembord is gesynchroniseerd"), ("Update client clipboard", "Klembord van client bijwerken"), + ("Untagged", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index b6cc5318ab86..872fd5bf713b 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -654,5 +654,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload files", "Wyślij pliki"), ("Clipboard is synchronized", "Schowek jest zsynchronizowany"), ("Update client clipboard", "Uaktualnij schowek klienta"), + ("Untagged", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index 3fe7951870a4..ad3a5f83171a 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -654,5 +654,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload files", ""), ("Clipboard is synchronized", ""), ("Update client clipboard", ""), + ("Untagged", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index f382b7aba22d..466952c023e0 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -654,5 +654,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload files", ""), ("Clipboard is synchronized", ""), ("Update client clipboard", ""), + ("Untagged", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ro.rs b/src/lang/ro.rs index 7aaef0e01c4f..830a26a4929a 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -654,5 +654,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload files", ""), ("Clipboard is synchronized", ""), ("Update client clipboard", ""), + ("Untagged", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index bcc5ed996740..582fdf6dcc6a 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -654,5 +654,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload files", "Загрузить файлы"), ("Clipboard is synchronized", "Буфер обмена синхронизирован"), ("Update client clipboard", "Обновить буфер обмена клиента"), + ("Untagged", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 50ba1aeb080a..d6bd0f7111f4 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -654,5 +654,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload files", ""), ("Clipboard is synchronized", ""), ("Update client clipboard", ""), + ("Untagged", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sl.rs b/src/lang/sl.rs index 4e52bbe40fcb..3fbf30ba3011 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -654,5 +654,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload files", ""), ("Clipboard is synchronized", ""), ("Update client clipboard", ""), + ("Untagged", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sq.rs b/src/lang/sq.rs index abab6acd8936..b68e6a1e03bb 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -654,5 +654,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload files", ""), ("Clipboard is synchronized", ""), ("Update client clipboard", ""), + ("Untagged", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sr.rs b/src/lang/sr.rs index 96bf3e1e06da..9def539e59a9 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -654,5 +654,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload files", ""), ("Clipboard is synchronized", ""), ("Update client clipboard", ""), + ("Untagged", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sv.rs b/src/lang/sv.rs index 69806fa7f6f9..af00b788ed73 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -654,5 +654,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload files", ""), ("Clipboard is synchronized", ""), ("Update client clipboard", ""), + ("Untagged", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index 1b0cf69e4f25..ce3e99abdee0 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -654,5 +654,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload files", ""), ("Clipboard is synchronized", ""), ("Update client clipboard", ""), + ("Untagged", ""), ].iter().cloned().collect(); } diff --git a/src/lang/th.rs b/src/lang/th.rs index a657201e993b..09fde8671f81 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -654,5 +654,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload files", ""), ("Clipboard is synchronized", ""), ("Update client clipboard", ""), + ("Untagged", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index 1b7b783d30f7..b7f9750f1e8b 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -654,5 +654,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload files", ""), ("Clipboard is synchronized", ""), ("Update client clipboard", ""), + ("Untagged", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index fb9259ef9b67..e99ee8d36461 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -654,5 +654,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload files", "上傳檔案"), ("Clipboard is synchronized", "剪貼簿已同步"), ("Update client clipboard", ""), + ("Untagged", ""), ].iter().cloned().collect(); } diff --git a/src/lang/uk.rs b/src/lang/uk.rs index ad83430505e6..6b5ec4381887 100644 --- a/src/lang/uk.rs +++ b/src/lang/uk.rs @@ -654,5 +654,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload files", "Надіслати файли"), ("Clipboard is synchronized", "Буфер обміну синхронізовано"), ("Update client clipboard", "Оновити буфер обміну клієнта"), + ("Untagged", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index 0d4751cd4f91..1970e17ca9c9 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -654,5 +654,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload files", ""), ("Clipboard is synchronized", ""), ("Update client clipboard", ""), + ("Untagged", ""), ].iter().cloned().collect(); } From 3c7f6d3127e7c35c0b949d5299e271bc27b7d708 Mon Sep 17 00:00:00 2001 From: bovirus <1262554+bovirus@users.noreply.github.com> Date: Wed, 27 Nov 2024 00:37:49 +0100 Subject: [PATCH 419/541] Italian language update (#10067) --- src/lang/it.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lang/it.rs b/src/lang/it.rs index 5bec55f4f62e..838df4d99726 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -654,6 +654,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload files", "File upload"), ("Clipboard is synchronized", "Gli appunti sono sincronizzati"), ("Update client clipboard", "Aggiorna appunti client"), - ("Untagged", ""), + ("Untagged", "Senza tag"), ].iter().cloned().collect(); } From 734fb8d6f7a226786efb533e30e3ee75b4246fc3 Mon Sep 17 00:00:00 2001 From: BoyChai <1972567225@qq.com> Date: Wed, 27 Nov 2024 10:47:09 +0800 Subject: [PATCH 420/541] Update README-ZH.md (#10069) Modify Alibaba Cloud apt source --- docs/README-ZH.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/README-ZH.md b/docs/README-ZH.md index 5a5f56b204e0..4920ade6d960 100644 --- a/docs/README-ZH.md +++ b/docs/README-ZH.md @@ -135,8 +135,8 @@ docker build -t "rustdesk-builder" . # 构建容器 ``` 在Dockerfile的RUN apt update之前插入两行: - RUN sed -i "s/deb.debian.org/mirrors.163.com/g" /etc/apt/sources.list - RUN sed -i "s/security.debian.org/mirrors.163.com/g" /etc/apt/sources.list + RUN sed -i "s|deb.debian.org|mirrors.aliyun.com|g" /etc/apt/sources.list && \ + sed -i "s|security.debian.org|mirrors.aliyun.com|g" /etc/apt/sources.list ``` 2. 修改容器系统中的 cargo 源,在`RUN ./rustup.sh -y`后插入下面代码: From 2cf43042e659352e5c3b4e9375d5cf39e080ed45 Mon Sep 17 00:00:00 2001 From: Leo Mozoloa Date: Wed, 27 Nov 2024 16:00:41 +0100 Subject: [PATCH 421/541] Update fr.rs (#10075) --- src/lang/fr.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 9366c6a19309..774e03865c2f 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -145,7 +145,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Failed to make direct connection to remote desktop", "Impossible d'établir une connexion directe"), ("Set Password", "Définir le mot de passe"), ("OS Password", "Mot de passe du système d'exploitation"), - ("install_tip", "Vous utilisez une version non installée. En raison des restrictions UAC, en tant que terminal contrôlé, dans certains cas, il ne sera pas en mesure de contrôler la souris et le clavier ou d'enregistrer l'écran. Veuillez cliquer sur le bouton ci-dessous pour installer RustDesk au système pour éviter la question ci-dessus."), + ("install_tip", "RustDesk n'est pas installé, ce qui peut limiter son utilisation à cause de l'UAC. Cliquez ci-dessous pour l'installer."), ("Click to upgrade", "Cliquer pour mettre à niveau"), ("Click to download", "Cliquer pour télécharger"), ("Click to update", "Cliquer pour mettre à jour"), From 4f86169f7f2f2e643ef230e17818e58c2ce35633 Mon Sep 17 00:00:00 2001 From: XLion Date: Thu, 28 Nov 2024 00:10:47 +0800 Subject: [PATCH 422/541] Update tw.rs (#10076) --- src/lang/tw.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lang/tw.rs b/src/lang/tw.rs index e99ee8d36461..8a42a331182f 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -653,7 +653,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", "上傳資料夾"), ("Upload files", "上傳檔案"), ("Clipboard is synchronized", "剪貼簿已同步"), - ("Update client clipboard", ""), - ("Untagged", ""), + ("Update client clipboard", "更新客戶端的剪貼簿"), + ("Untagged", "無標籤"), ].iter().cloned().collect(); } From afc8bb71dc1c738d92c4fa6cfef9e999a916cdf5 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Fri, 29 Nov 2024 00:56:38 +0800 Subject: [PATCH 423/541] feat: mobile, key help tool, more keys (#10068) * feat: mobile, key help tool, vk_enter Signed-off-by: fufesou * Mobile, add more function keys Signed-off-by: fufesou * Mobile, more virtual function keys Signed-off-by: fufesou * uinput, menu maps key_compose Signed-off-by: fufesou --------- Signed-off-by: fufesou --- flutter/lib/mobile/pages/remote_page.dart | 24 +++++++++++++++++++++++ libs/enigo/src/linux/nix_impl.rs | 2 +- src/client.rs | 1 + src/server/uinput.rs | 4 ++-- 4 files changed, 28 insertions(+), 3 deletions(-) diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index 4457cbe26a26..fa7c35bb64e2 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -872,6 +872,8 @@ class _KeyHelpToolsState extends State { final pi = gFFI.ffiModel.pi; final isMac = pi.platform == kPeerPlatformMacOS; + final isWin = pi.platform == kPeerPlatformWindows; + final isLinux = pi.platform == kPeerPlatformLinux; final modifiers = [ wrap('Ctrl ', () { setState(() => inputModel.ctrl = !inputModel.ctrl); @@ -952,6 +954,28 @@ class _KeyHelpToolsState extends State { wrap('PgDn', () { inputModel.inputKey('VK_NEXT'); }), + // to-do: support PrtScr on Mac + if (isWin || isLinux) + wrap('PrtScr', () { + inputModel.inputKey('VK_SNAPSHOT'); + }), + if (isWin || isLinux) + wrap('ScrollLock', () { + inputModel.inputKey('VK_SCROLL'); + }), + if (isWin || isLinux) + wrap('Pause', () { + inputModel.inputKey('VK_PAUSE'); + }), + if (isWin || isLinux) + // Maybe it's better to call it "Menu" + // https://en.wikipedia.org/wiki/Menu_key + wrap('Menu', () { + inputModel.inputKey('Apps'); + }), + wrap('Enter', () { + inputModel.inputKey('VK_ENTER'); + }), SizedBox(width: 9999), wrap('', () { inputModel.inputKey('VK_LEFT'); diff --git a/libs/enigo/src/linux/nix_impl.rs b/libs/enigo/src/linux/nix_impl.rs index c082236e3f5d..902d77948e28 100644 --- a/libs/enigo/src/linux/nix_impl.rs +++ b/libs/enigo/src/linux/nix_impl.rs @@ -345,7 +345,7 @@ fn convert_to_tfc_key(key: Key) -> Option { Key::Numpad9 => TFC_Key::N9, Key::Decimal => TFC_Key::NumpadDecimal, Key::Clear => TFC_Key::NumpadClear, - Key::Pause => TFC_Key::PlayPause, + Key::Pause => TFC_Key::Pause, Key::Print => TFC_Key::Print, Key::Snapshot => TFC_Key::PrintScreen, Key::Insert => TFC_Key::Insert, diff --git a/src/client.rs b/src/client.rs index 97fa3a0424a2..474c7fdfc0b8 100644 --- a/src/client.rs +++ b/src/client.rs @@ -3293,6 +3293,7 @@ lazy_static::lazy_static! { ("VK_PRINT", Key::ControlKey(ControlKey::Print)), ("VK_EXECUTE", Key::ControlKey(ControlKey::Execute)), ("VK_SNAPSHOT", Key::ControlKey(ControlKey::Snapshot)), + ("VK_SCROLL", Key::ControlKey(ControlKey::Scroll)), ("VK_INSERT", Key::ControlKey(ControlKey::Insert)), ("VK_DELETE", Key::ControlKey(ControlKey::Delete)), ("VK_HELP", Key::ControlKey(ControlKey::Help)), diff --git a/src/server/uinput.rs b/src/server/uinput.rs index 60c647862ad7..894ce82f90d7 100644 --- a/src/server/uinput.rs +++ b/src/server/uinput.rs @@ -239,7 +239,7 @@ pub mod service { (enigo::Key::Select, evdev::Key::KEY_SELECT), (enigo::Key::Print, evdev::Key::KEY_PRINT), // (enigo::Key::Execute, evdev::Key::KEY_EXECUTE), - // (enigo::Key::Snapshot, evdev::Key::KEY_SNAPSHOT), + (enigo::Key::Snapshot, evdev::Key::KEY_SYSRQ), (enigo::Key::Insert, evdev::Key::KEY_INSERT), (enigo::Key::Help, evdev::Key::KEY_HELP), (enigo::Key::Sleep, evdev::Key::KEY_SLEEP), @@ -247,7 +247,7 @@ pub mod service { (enigo::Key::Scroll, evdev::Key::KEY_SCROLLLOCK), (enigo::Key::NumLock, evdev::Key::KEY_NUMLOCK), (enigo::Key::RWin, evdev::Key::KEY_RIGHTMETA), - (enigo::Key::Apps, evdev::Key::KEY_CONTEXT_MENU), + (enigo::Key::Apps, evdev::Key::KEY_COMPOSE), // it's a little strange that the key is mapped to KEY_COMPOSE, not KEY_MENU (enigo::Key::Multiply, evdev::Key::KEY_KPASTERISK), (enigo::Key::Add, evdev::Key::KEY_KPPLUS), (enigo::Key::Subtract, evdev::Key::KEY_KPMINUS), From b91b49229aa827d0d2b425d1ff1ad7ba9a0040f4 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Fri, 29 Nov 2024 19:10:57 +0800 Subject: [PATCH 424/541] enable our engine to fix dart supporting win7, https://github.com/rustdesk/rustdesk/issues/10085#issuecomment-2506485955 --- .github/workflows/flutter-build.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/flutter-build.yml b/.github/workflows/flutter-build.yml index aefd06e7d536..555bcb96b0cc 100644 --- a/.github/workflows/flutter-build.yml +++ b/.github/workflows/flutter-build.yml @@ -107,7 +107,6 @@ jobs: # https://github.com/flutter/flutter/issues/155685 - name: Replace engine with rustdesk custom flutter engine - if: false run: | flutter doctor -v flutter precache --windows From b32ff87c6e059fcb377d1e09de48827a1f746df3 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Fri, 29 Nov 2024 22:39:07 +0800 Subject: [PATCH 425/541] fix: android, pan, canvas, remove `toInt()` (#10103) Signed-off-by: fufesou --- flutter/lib/models/input_model.dart | 40 +++++++++++++---------------- flutter/lib/models/model.dart | 10 ++++---- 2 files changed, 23 insertions(+), 27 deletions(-) diff --git a/flutter/lib/models/input_model.dart b/flutter/lib/models/input_model.dart index 0692ba2df4d4..a30bb79fdbd3 100644 --- a/flutter/lib/models/input_model.dart +++ b/flutter/lib/models/input_model.dart @@ -1080,7 +1080,7 @@ class InputModel { onExit: true, ); - static int tryGetNearestRange(int v, int min, int max, int n) { + static double tryGetNearestRange(double v, double min, double max, double n) { if (v < min && v >= min - n) { v = min; } @@ -1138,8 +1138,8 @@ class InputModel { return; } evtValue = { - 'x': pos.x, - 'y': pos.y, + 'x': pos.x.toInt(), + 'y': pos.y.toInt(), }; } @@ -1221,8 +1221,8 @@ class InputModel { evt['x'] = '0'; evt['y'] = '0'; } else { - evt['x'] = '${pos.x}'; - evt['y'] = '${pos.y}'; + evt['x'] = '${pos.x.toInt()}'; + evt['y'] = '${pos.y.toInt()}'; } Map mapButtons = { @@ -1362,31 +1362,27 @@ class InputModel { y = pos.dy; } - var evtX = 0; - var evtY = 0; - try { - evtX = x.round(); - evtY = y.round(); - } catch (e) { - debugPrintStack(label: 'canvas.scale value ${canvas.scale}, $e'); - return null; - } - return InputModel.getPointInRemoteRect( - true, peerPlatform, kind, evtType, evtX, evtY, rect, + true, peerPlatform, kind, evtType, x, y, rect, buttons: buttons); } - static Point? getPointInRemoteRect(bool isLocalDesktop, String? peerPlatform, - String kind, String evtType, int evtX, int evtY, Rect rect, + static Point? getPointInRemoteRect( + bool isLocalDesktop, + String? peerPlatform, + String kind, + String evtType, + double evtX, + double evtY, + Rect rect, {int buttons = kPrimaryMouseButton}) { - int minX = rect.left.toInt(); + double minX = rect.left; // https://github.com/rustdesk/rustdesk/issues/6678 // For Windows, [0,maxX], [0,maxY] should be set to enable window snapping. - int maxX = (rect.left + rect.width).toInt() - + double maxX = (rect.left + rect.width) - (peerPlatform == kPeerPlatformWindows ? 0 : 1); - int minY = rect.top.toInt(); - int maxY = (rect.top + rect.height).toInt() - + double minY = rect.top; + double maxY = (rect.top + rect.height) - (peerPlatform == kPeerPlatformWindows ? 0 : 1); evtX = InputModel.tryGetNearestRange(evtX, minX, maxX, 5); evtY = InputModel.tryGetNearestRange(evtY, minY, maxY, 5); diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 2c04eae6c87f..fdcd28f8d04c 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -2184,7 +2184,7 @@ class CursorModel with ChangeNotifier { if (dx == 0 && dy == 0) return; - Point? newPos; + Point? newPos; final rect = parent.target?.ffiModel.rect; if (rect == null) { // unreachable @@ -2195,8 +2195,8 @@ class CursorModel with ChangeNotifier { parent.target?.ffiModel.pi.platform, kPointerEventKindMouse, kMouseEventTypeDefault, - (_x + dx).toInt(), - (_y + dy).toInt(), + _x + dx, + _y + dy, rect, buttons: kPrimaryButton); if (newPos == null) { @@ -2204,8 +2204,8 @@ class CursorModel with ChangeNotifier { } dx = newPos.x - _x; dy = newPos.y - _y; - _x = newPos.x.toDouble(); - _y = newPos.y.toDouble(); + _x = newPos.x; + _y = newPos.y; if (tryMoveCanvasX && dx != 0) { parent.target?.canvasModel.panX(-dx * scale); } From d3f0c80e94657285da23b0bd42b85b16c20a8c40 Mon Sep 17 00:00:00 2001 From: 21pages Date: Sat, 30 Nov 2024 09:24:05 +0800 Subject: [PATCH 426/541] "Untagged" tag uses the theme accent color (#10111) Signed-off-by: 21pages --- flutter/lib/models/ab_model.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/flutter/lib/models/ab_model.dart b/flutter/lib/models/ab_model.dart index 613ec1ed350d..3aa722a5abf1 100644 --- a/flutter/lib/models/ab_model.dart +++ b/flutter/lib/models/ab_model.dart @@ -648,6 +648,9 @@ class AbModel { } Color getCurrentAbTagColor(String tag) { + if (tag == kUntagged) { + return MyTheme.accent; + } int? colorValue = current.tagColors[tag]; if (colorValue != null) { return Color(colorValue); From e0ed6ee986e6b68cacbfdaefd5bf306e935e73f6 Mon Sep 17 00:00:00 2001 From: Vasyl Gello Date: Sat, 30 Nov 2024 03:24:45 +0200 Subject: [PATCH 427/541] Fix F-Droid build and bump Android NDK to r27c (#10105) * Fix fdroid build * Refactor recent @fufesou edits to reflect the fact that .gclient file is needed only on x86 to build jit-release version of flutter-engine and `flutter-sdk` directory is not affected by flutter version checkouts * Install cargo-ndk and flutter-rust-codegen with `--locked` argument to avoid bumping `cargo-platform` to require newer Rust toolchain Signed-off-by: Vasyl Gello * Bump Android NDK to r27c Signed-off-by: Vasyl Gello --------- Signed-off-by: Vasyl Gello --- .github/workflows/flutter-build.yml | 2 +- flutter/build_fdroid.sh | 141 +++++++++++++++++++--------- 2 files changed, 97 insertions(+), 46 deletions(-) diff --git a/.github/workflows/flutter-build.yml b/.github/workflows/flutter-build.yml index 555bcb96b0cc..fd39302163f3 100644 --- a/.github/workflows/flutter-build.yml +++ b/.github/workflows/flutter-build.yml @@ -34,7 +34,7 @@ env: # vcpkg version: 2024.07.12 VCPKG_COMMIT_ID: "1de2026f28ead93ff1773e6e680387643e914ea1" VERSION: "1.3.3" - NDK_VERSION: "r27b" + NDK_VERSION: "r27c" #signing keys env variable checks ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}" MACOS_P12_BASE64: "${{ secrets.MACOS_P12_BASE64 }}" diff --git a/flutter/build_fdroid.sh b/flutter/build_fdroid.sh index e0a4eb5991b9..1821c529afb5 100755 --- a/flutter/build_fdroid.sh +++ b/flutter/build_fdroid.sh @@ -1,7 +1,5 @@ #!/bin/bash -set -x - # # Script to build F-Droid release of RustDesk # @@ -23,6 +21,43 @@ set -x # + build: perform actual build of APK file # +# Start of functions + +# Install Flutter of version `VERSION` from Github repository +# into directory `FLUTTER_DIR` and apply patches if needed + +prepare_flutter() { + VERSION="${1}" + FLUTTER_DIR="${2}" + + if [ ! -f "${FLUTTER_DIR}/bin/flutter" ]; then + git clone https://github.com/flutter/flutter "${FLUTTER_DIR}" + fi + + pushd "${FLUTTER_DIR}" + + git restore . + git checkout "${VERSION}" + + # Patch flutter + + if dpkg --compare-versions "${VERSION}" ge "3.24.4"; then + git apply "${ROOTDIR}/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff" + fi + + flutter config --no-analytics + + popd # ${FLUTTER_DIR} +} + +# Start of script + +set -x + +# Note current working directory as root dir for patches + +ROOTDIR="${PWD}" + # Parse command-line arguments VERNAME="${1}" @@ -82,21 +117,6 @@ export PATH="${PATH}:${HOME}/flutter/bin:${HOME}/depot_tools" export VCPKG_ROOT="${HOME}/vcpkg" -prepare_Flutter() { - version="${1}" - - pushd "${HOME}" - if [ ! -f "${HOME}/flutter/bin/flutter" ]; then - git clone https://github.com/flutter/flutter - fi - pushd flutter - git restore . - git checkout "${version}" - flutter config --no-analytics - popd # flutter - popd # ${HOME} -} - # Now act depending on build step # NOTE: F-Droid maintainers require explicit declaration of dependencies @@ -116,15 +136,20 @@ prebuild) .env.CARGO_NDK_VERSION \ .github/workflows/flutter-build.yml)" + # Flutter used to compile main Rustdesk library + 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 used to compile Flutter<->Rust bridge files + FLUTTER_BRIDGE_VERSION="$(yq -r \ .env.FLUTTER_VERSION \ .github/workflows/bridge.yml)" @@ -207,14 +232,16 @@ prebuild) cargo install \ cargo-ndk \ - --version "${CARGO_NDK_VERSION}" + --version "${CARGO_NDK_VERSION}" \ + --locked # Install rust bridge generator cargo install cargo-expand cargo install flutter_rust_bridge_codegen \ --version "${FLUTTER_RUST_BRIDGE_VERSION}" \ - --features "uuid" + --features "uuid" \ + --locked # Populate native vcpkg dependencies @@ -277,46 +304,66 @@ prebuild) git apply res/fdroid/patches/*.patch - # Backup .gclient file, for later restore + # If Flutter version used to generate bridge files differs from Flutter + # version used to compile Rustdesk library, generate bridge using the + # `FLUTTER_BRIDGE_VERSION` an restore the pubspec later - cp flutter-sdk/.gclient flutter-sdk/.gclient.bak + if [ "${FLUTTER_VERSION}" != "${FLUTTER_BRIDGE_VERSION}" ]; then + # Install Flutter bridge version - # For FLUTTER_BRIDGE_VERSION - sed \ - -i \ - -e 's/extended_text: 14.0.0/extended_text: 13.0.0/g' \ - flutter/pubspec.yaml + prepare_flutter "${FLUTTER_BRIDGE_VERSION}" "${HOME}/flutter" - # Install Flutter bridge version - prepare_Flutter "${FLUTTER_BRIDGE_VERSION}" - cp flutter-sdk/.gclient.bak flutter-sdk/.gclient - sed -i "s/FLUTTER_VERSION_PLACEHOLDER/${FLUTTER_BRIDGE_VERSION}/" flutter-sdk/.gclient + # Save changes - # Download Flutter dependencies - pushd flutter - flutter clean && flutter packages pub get - popd # flutter + git add . + + # Edit pubspec to make flutter bridge version work + + sed \ + -i \ + -e 's/extended_text: 14.0.0/extended_text: 13.0.0/g' \ + flutter/pubspec.yaml - # Generate FFI bindings - flutter_rust_bridge_codegen \ - --rust-input ./src/flutter_ffi.rs \ - --dart-output ./flutter/lib/generated_bridge.dart + # Download Flutter dependencies - git restore flutter/pubspec.* + pushd flutter - # Install Flutter - prepare_Flutter "${FLUTTER_VERSION}" - cp flutter-sdk/.gclient.bak flutter-sdk/.gclient - sed -i "s/FLUTTER_VERSION_PLACEHOLDER/${FLUTTER_VERSION}/" flutter-sdk/.gclient + flutter clean + 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 + + # Add bridge files to save-list + + git add -f ./flutter/lib/generated_bridge.* ./src/bridge_generated.* + + # Restore everything + + git checkout '*' + git clean -dffx + git reset + fi + + # Install Flutter version for RustDesk library build + + prepare_flutter "${FLUTTER_VERSION}" "${HOME}/flutter" # gms is not in thoes files now, but we still keep the following line for future reference(maybe). + sed \ -i \ -e '/gms/d' \ flutter/android/build.gradle \ flutter/android/app/build.gradle - # `firebase_analytics`` is not in thoes files now, but we still keep the following lines. + # `firebase_analytics` is not in these files now, but we still keep the following lines. + sed \ -i \ -e '/firebase_analytics/d' \ @@ -343,9 +390,12 @@ build) # '.github/workflows/flutter-build.yml' # + # Flutter used to compile main Rustdesk library + 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 \ @@ -381,7 +431,8 @@ build) pushd flutter - flutter clean && flutter packages pub get + flutter clean + flutter packages pub get popd # flutter From d60b5a6ca097916758d8dfdd2e89b2ec3b581dbf Mon Sep 17 00:00:00 2001 From: 21pages Date: Sat, 30 Nov 2024 11:44:51 +0800 Subject: [PATCH 428/541] videotoolbox/mediacodec support changing bitrate dynamically (#10117) Signed-off-by: 21pages --- .github/workflows/bridge.yml | 2 +- .github/workflows/flutter-build.yml | 2 +- .github/workflows/playground.yml | 6 +- libs/scrap/src/common/hwcodec.rs | 8 +- .../0004-videotoolbox-changing-bitrate.patch | 84 ++++++ .../0005-mediacodec-changing-bitrate.patch | 259 ++++++++++++++++++ res/vcpkg/ffmpeg/portfile.cmake | 2 + 7 files changed, 352 insertions(+), 11 deletions(-) create mode 100644 res/vcpkg/ffmpeg/patch/0004-videotoolbox-changing-bitrate.patch create mode 100644 res/vcpkg/ffmpeg/patch/0005-mediacodec-changing-bitrate.patch diff --git a/.github/workflows/bridge.yml b/.github/workflows/bridge.yml index 1c0fec8d21f9..9d56fbe6f2b5 100644 --- a/.github/workflows/bridge.yml +++ b/.github/workflows/bridge.yml @@ -73,7 +73,7 @@ jobs: - name: Install flutter rust bridge deps shell: bash run: | - cargo install flutter_rust_bridge_codegen --version ${{ env.FLUTTER_RUST_BRIDGE_VERSION }} --features "uuid" + cargo install flutter_rust_bridge_codegen --version ${{ env.FLUTTER_RUST_BRIDGE_VERSION }} --features "uuid" --locked pushd flutter && sed -i -e 's/extended_text: 14.0.0/extended_text: 13.0.0/g' pubspec.yaml && flutter pub get && popd - name: Run flutter rust bridge diff --git a/.github/workflows/flutter-build.yml b/.github/workflows/flutter-build.yml index fd39302163f3..b295e70f6f87 100644 --- a/.github/workflows/flutter-build.yml +++ b/.github/workflows/flutter-build.yml @@ -1032,7 +1032,7 @@ jobs: 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 }} + cargo install cargo-ndk --version ${{ env.CARGO_NDK_VERSION }} --locked case ${{ matrix.job.target }} in aarch64-linux-android) ./flutter/ndk_arm64.sh diff --git a/.github/workflows/playground.yml b/.github/workflows/playground.yml index 843e07835cdb..9d4d42cb3eda 100644 --- a/.github/workflows/playground.yml +++ b/.github/workflows/playground.yml @@ -149,7 +149,7 @@ jobs: 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" + cargo install flutter_rust_bridge_codegen --version ${{ matrix.job.bridge }} --features "uuid" --locked # 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 @@ -302,7 +302,7 @@ jobs: - 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" + cargo install flutter_rust_bridge_codegen --version ${{ env.FLUTTER_RUST_BRIDGE_VERSION }} --features "uuid" --locked 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 @@ -347,7 +347,7 @@ jobs: 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 }} + cargo install cargo-ndk --version ${{ env.CARGO_NDK_VERSION }} --locked case ${{ matrix.job.target }} in aarch64-linux-android) ./flutter/ndk_arm64.sh diff --git a/libs/scrap/src/common/hwcodec.rs b/libs/scrap/src/common/hwcodec.rs index d929024d84eb..3d56472eedc4 100644 --- a/libs/scrap/src/common/hwcodec.rs +++ b/libs/scrap/src/common/hwcodec.rs @@ -193,15 +193,11 @@ impl EncoderApi for HwRamEncoder { } fn support_abr(&self) -> bool { - ["qsv", "vaapi", "mediacodec", "videotoolbox"] - .iter() - .all(|&x| !self.config.name.contains(x)) + ["qsv", "vaapi"].iter().all(|&x| !self.config.name.contains(x)) } fn support_changing_quality(&self) -> bool { - ["vaapi", "mediacodec", "videotoolbox"] - .iter() - .all(|&x| !self.config.name.contains(x)) + ["vaapi"].iter().all(|&x| !self.config.name.contains(x)) } fn latency_free(&self) -> bool { diff --git a/res/vcpkg/ffmpeg/patch/0004-videotoolbox-changing-bitrate.patch b/res/vcpkg/ffmpeg/patch/0004-videotoolbox-changing-bitrate.patch new file mode 100644 index 000000000000..a0b337c5bae5 --- /dev/null +++ b/res/vcpkg/ffmpeg/patch/0004-videotoolbox-changing-bitrate.patch @@ -0,0 +1,84 @@ +From 7f12898fe8fd12c1042c98b34825ab2eda89e54d Mon Sep 17 00:00:00 2001 +From: 21pages +Date: Sun, 24 Nov 2024 12:58:39 +0800 +Subject: [PATCH 1/2] videotoolbox changing bitrate + +Signed-off-by: 21pages +--- + libavcodec/videotoolboxenc.c | 39 ++++++++++++++++++++++++++++++++++++ + 1 file changed, 39 insertions(+) + +diff --git a/libavcodec/videotoolboxenc.c b/libavcodec/videotoolboxenc.c +index 5ea9afee22..89c927cdcc 100644 +--- a/libavcodec/videotoolboxenc.c ++++ b/libavcodec/videotoolboxenc.c +@@ -278,6 +278,8 @@ typedef struct VTEncContext { + int max_slice_bytes; + int power_efficient; + int max_ref_frames; ++ ++ int last_bit_rate; + } VTEncContext; + + static int vt_dump_encoder(AVCodecContext *avctx) +@@ -1174,6 +1176,7 @@ static int vtenc_create_encoder(AVCodecContext *avctx, + int64_t one_second_value = 0; + void *nums[2]; + ++ vtctx->last_bit_rate = bit_rate; + int status = VTCompressionSessionCreate(kCFAllocatorDefault, + avctx->width, + avctx->height, +@@ -2618,6 +2621,41 @@ static int vtenc_send_frame(AVCodecContext *avctx, + return 0; + } + ++static void update_config(AVCodecContext *avctx) ++{ ++ VTEncContext *vtctx = avctx->priv_data; ++ ++ if (avctx->codec_id != AV_CODEC_ID_PRORES) { ++ if (avctx->bit_rate != vtctx->last_bit_rate) { ++ av_log(avctx, AV_LOG_INFO, "Setting bit rate to %d\n", avctx->bit_rate); ++ vtctx->last_bit_rate = avctx->bit_rate; ++ SInt32 bit_rate = avctx->bit_rate; ++ CFNumberRef bit_rate_num = CFNumberCreate(kCFAllocatorDefault, ++ kCFNumberSInt32Type, ++ &bit_rate); ++ if (!bit_rate_num) return; ++ ++ if (vtctx->constant_bit_rate) { ++ int status = VTSessionSetProperty(vtctx->session, ++ compat_keys.kVTCompressionPropertyKey_ConstantBitRate, ++ bit_rate_num); ++ if (status == kVTPropertyNotSupportedErr) { ++ av_log(avctx, AV_LOG_ERROR, "Error: -constant_bit_rate true is not supported by the encoder.\n"); ++ } ++ } else { ++ int status = VTSessionSetProperty(vtctx->session, ++ kVTCompressionPropertyKey_AverageBitRate, ++ bit_rate_num); ++ if (!status) { ++ av_log(avctx, AV_LOG_ERROR, "Error: cannot set average bit rate: %d\n", status); ++ } ++ } ++ ++ CFRelease(bit_rate_num); ++ } ++ } ++} ++ + static av_cold int vtenc_frame( + AVCodecContext *avctx, + AVPacket *pkt, +@@ -2630,6 +2668,7 @@ static av_cold int vtenc_frame( + CMSampleBufferRef buf = NULL; + ExtraSEI *sei = NULL; + ++ update_config(avctx); + if (frame) { + status = vtenc_send_frame(avctx, vtctx, frame); + +-- +2.43.0.windows.1 + diff --git a/res/vcpkg/ffmpeg/patch/0005-mediacodec-changing-bitrate.patch b/res/vcpkg/ffmpeg/patch/0005-mediacodec-changing-bitrate.patch new file mode 100644 index 000000000000..90c3613a43b0 --- /dev/null +++ b/res/vcpkg/ffmpeg/patch/0005-mediacodec-changing-bitrate.patch @@ -0,0 +1,259 @@ +From fb5cc7909a9b288f6bd13c75992b66ed257ab019 Mon Sep 17 00:00:00 2001 +From: 21pages +Date: Sun, 24 Nov 2024 14:17:39 +0800 +Subject: [PATCH 2/2] mediacodec changing bitrate + +Signed-off-by: 21pages +--- + libavcodec/mediacodec_wrapper.c | 96 +++++++++++++++++++++++++++++++++ + libavcodec/mediacodec_wrapper.h | 7 +++ + libavcodec/mediacodecenc.c | 18 +++++++ + 3 files changed, 121 insertions(+) + +diff --git a/libavcodec/mediacodec_wrapper.c b/libavcodec/mediacodec_wrapper.c +index 306359071e..44fdd71869 100644 +--- a/libavcodec/mediacodec_wrapper.c ++++ b/libavcodec/mediacodec_wrapper.c +@@ -35,6 +35,8 @@ + #include "ffjni.h" + #include "mediacodec_wrapper.h" + ++#define PARAMETER_KEY_VIDEO_BITRATE "video-bitrate" ++ + struct JNIAMediaCodecListFields { + + jclass mediacodec_list_class; +@@ -195,6 +197,8 @@ struct JNIAMediaCodecFields { + jmethodID set_input_surface_id; + jmethodID signal_end_of_input_stream_id; + ++ jmethodID set_parameters_id; ++ + jclass mediainfo_class; + + jmethodID init_id; +@@ -248,6 +252,8 @@ static const struct FFJniField jni_amediacodec_mapping[] = { + { "android/media/MediaCodec", "setInputSurface", "(Landroid/view/Surface;)V", FF_JNI_METHOD, OFFSET(set_input_surface_id), 0 }, + { "android/media/MediaCodec", "signalEndOfInputStream", "()V", FF_JNI_METHOD, OFFSET(signal_end_of_input_stream_id), 0 }, + ++ { "android/media/MediaCodec", "setParameters", "(Landroid/os/Bundle;)V", FF_JNI_METHOD, OFFSET(set_parameters_id), 0 }, ++ + { "android/media/MediaCodec$BufferInfo", NULL, NULL, FF_JNI_CLASS, OFFSET(mediainfo_class), 1 }, + + { "android/media/MediaCodec.BufferInfo", "", "()V", FF_JNI_METHOD, OFFSET(init_id), 1 }, +@@ -292,6 +298,24 @@ typedef struct FFAMediaCodecJni { + + static const FFAMediaCodec media_codec_jni; + ++struct JNIABundleFields ++{ ++ jclass bundle_class; ++ jmethodID init_id; ++ jmethodID put_int_id; ++}; ++ ++#define OFFSET(x) offsetof(struct JNIABundleFields, x) ++static const struct FFJniField jni_abundle_mapping[] = { ++ { "android/os/Bundle", NULL, NULL, FF_JNI_CLASS, OFFSET(bundle_class), 1 }, ++ ++ { "android/os/Bundle", "", "()V", FF_JNI_METHOD, OFFSET(init_id), 1 }, ++ { "android/os/Bundle", "putInt", "(Ljava/lang/String;I)V", FF_JNI_METHOD, OFFSET(put_int_id), 1 }, ++ ++ { NULL } ++}; ++#undef OFFSET ++ + #define JNI_GET_ENV_OR_RETURN(env, log_ctx, ret) do { \ + (env) = ff_jni_get_env(log_ctx); \ + if (!(env)) { \ +@@ -1761,6 +1785,64 @@ static int mediacodec_jni_signalEndOfInputStream(FFAMediaCodec *ctx) + return 0; + } + ++static int mediacodec_jni_setParameter(FFAMediaCodec *ctx, const char* name, int value) ++{ ++ JNIEnv *env = NULL; ++ struct JNIABundleFields jfields = { 0 }; ++ jobject object = NULL; ++ jstring key = NULL; ++ FFAMediaCodecJni *codec = (FFAMediaCodecJni *)ctx; ++ void *log_ctx = codec; ++ int ret = -1; ++ ++ JNI_GET_ENV_OR_RETURN(env, codec, AVERROR_EXTERNAL); ++ ++ if (ff_jni_init_jfields(env, &jfields, jni_abundle_mapping, 0, log_ctx) < 0) { ++ av_log(log_ctx, AV_LOG_ERROR, "Failed to init jfields\n"); ++ goto fail; ++ } ++ ++ object = (*env)->NewObject(env, jfields.bundle_class, jfields.init_id); ++ if (!object) { ++ av_log(log_ctx, AV_LOG_ERROR, "Failed to create bundle object\n"); ++ goto fail; ++ } ++ ++ key = ff_jni_utf_chars_to_jstring(env, name, log_ctx); ++ if (!key) { ++ av_log(log_ctx, AV_LOG_ERROR, "Failed to convert key to jstring\n"); ++ goto fail; ++ } ++ ++ (*env)->CallVoidMethod(env, object, jfields.put_int_id, key, value); ++ if (ff_jni_exception_check(env, 1, log_ctx) < 0) { ++ goto fail; ++ } ++ ++ (*env)->CallVoidMethod(env, codec->object, codec->jfields.set_parameters_id, object); ++ if (ff_jni_exception_check(env, 1, log_ctx) < 0) { ++ goto fail; ++ } ++ ++ ret = 0; ++ ++fail: ++ if (key) { ++ (*env)->DeleteLocalRef(env, key); ++ } ++ if (object) { ++ (*env)->DeleteLocalRef(env, object); ++ } ++ ff_jni_reset_jfields(env, &jfields, jni_abundle_mapping, 0, log_ctx); ++ ++ return ret; ++} ++ ++static int mediacodec_jni_setDynamicBitrate(FFAMediaCodec *ctx, int bitrate) ++{ ++ return mediacodec_jni_setParameter(ctx, PARAMETER_KEY_VIDEO_BITRATE, bitrate); ++} ++ + static const FFAMediaFormat media_format_jni = { + .class = &amediaformat_class, + +@@ -1820,6 +1902,8 @@ static const FFAMediaCodec media_codec_jni = { + .getConfigureFlagEncode = mediacodec_jni_getConfigureFlagEncode, + .cleanOutputBuffers = mediacodec_jni_cleanOutputBuffers, + .signalEndOfInputStream = mediacodec_jni_signalEndOfInputStream, ++ ++ .setDynamicBitrate = mediacodec_jni_setDynamicBitrate, + }; + + typedef struct FFAMediaFormatNdk { +@@ -1893,6 +1977,8 @@ typedef struct FFAMediaCodecNdk { + // Available since API level 26. + media_status_t (*setInputSurface)(AMediaCodec*, ANativeWindow *); + media_status_t (*signalEndOfInputStream)(AMediaCodec *); ++ ++ media_status_t (*setParameters)(AMediaCodec *, const AMediaFormat *format); + } FFAMediaCodecNdk; + + static const FFAMediaFormat media_format_ndk; +@@ -2154,6 +2240,8 @@ static inline FFAMediaCodec *ndk_codec_create(int method, const char *arg) { + GET_SYMBOL(setInputSurface, 0) + GET_SYMBOL(signalEndOfInputStream, 0) + ++ GET_SYMBOL(setParameters, 0) ++ + #undef GET_SYMBOL + + switch (method) { +@@ -2428,6 +2516,12 @@ static int mediacodec_ndk_signalEndOfInputStream(FFAMediaCodec *ctx) + return 0; + } + ++static int mediacodec_ndk_setDynamicBitrate(FFAMediaCodec *ctx, int bitrate) ++{ ++ av_log(ctx, AV_LOG_ERROR, "ndk setDynamicBitrate unavailable\n"); ++ return -1; ++} ++ + static const FFAMediaFormat media_format_ndk = { + .class = &amediaformat_ndk_class, + +@@ -2489,6 +2583,8 @@ static const FFAMediaCodec media_codec_ndk = { + .getConfigureFlagEncode = mediacodec_ndk_getConfigureFlagEncode, + .cleanOutputBuffers = mediacodec_ndk_cleanOutputBuffers, + .signalEndOfInputStream = mediacodec_ndk_signalEndOfInputStream, ++ ++ .setDynamicBitrate = mediacodec_ndk_setDynamicBitrate, + }; + + FFAMediaFormat *ff_AMediaFormat_new(int ndk) +diff --git a/libavcodec/mediacodec_wrapper.h b/libavcodec/mediacodec_wrapper.h +index 11a4260497..86c64556ad 100644 +--- a/libavcodec/mediacodec_wrapper.h ++++ b/libavcodec/mediacodec_wrapper.h +@@ -219,6 +219,8 @@ struct FFAMediaCodec { + + // For encoder with FFANativeWindow as input. + int (*signalEndOfInputStream)(FFAMediaCodec *); ++ ++ int (*setDynamicBitrate)(FFAMediaCodec *codec, int bitrate); + }; + + static inline char *ff_AMediaCodec_getName(FFAMediaCodec *codec) +@@ -343,6 +345,11 @@ static inline int ff_AMediaCodec_signalEndOfInputStream(FFAMediaCodec *codec) + return codec->signalEndOfInputStream(codec); + } + ++static inline int ff_AMediaCodec_setDynamicBitrate(FFAMediaCodec *codec, int bitrate) ++{ ++ return codec->setDynamicBitrate(codec, bitrate); ++} ++ + int ff_Build_SDK_INT(AVCodecContext *avctx); + + enum FFAMediaFormatColorRange { +diff --git a/libavcodec/mediacodecenc.c b/libavcodec/mediacodecenc.c +index d3bf27cb7f..621529d686 100644 +--- a/libavcodec/mediacodecenc.c ++++ b/libavcodec/mediacodecenc.c +@@ -73,6 +73,8 @@ typedef struct MediaCodecEncContext { + int bitrate_mode; + int level; + int pts_as_dts; ++ ++ int last_bit_rate; + } MediaCodecEncContext; + + enum { +@@ -155,6 +157,8 @@ static av_cold int mediacodec_init(AVCodecContext *avctx) + int ret; + int gop; + ++ s->last_bit_rate = avctx->bit_rate; ++ + if (s->use_ndk_codec < 0) + s->use_ndk_codec = !av_jni_get_java_vm(avctx); + +@@ -515,12 +519,26 @@ static int mediacodec_send(AVCodecContext *avctx, + return 0; + } + ++static void update_config(AVCodecContext *avctx) ++{ ++ MediaCodecEncContext *s = avctx->priv_data; ++ if (avctx->bit_rate != s->last_bit_rate) { ++ s->last_bit_rate = avctx->bit_rate; ++ if (0 != ff_AMediaCodec_setDynamicBitrate(s->codec, avctx->bit_rate)) { ++ av_log(avctx, AV_LOG_ERROR, "Failed to set bitrate to %d\n", avctx->bit_rate); ++ } else { ++ av_log(avctx, AV_LOG_INFO, "Set bitrate to %d\n", avctx->bit_rate); ++ } ++ } ++} ++ + static int mediacodec_encode(AVCodecContext *avctx, AVPacket *pkt) + { + MediaCodecEncContext *s = avctx->priv_data; + int ret; + int got_packet = 0; + ++ update_config(avctx); + // Return on three case: + // 1. Serious error + // 2. Got a packet success +-- +2.43.0.windows.1 + diff --git a/res/vcpkg/ffmpeg/portfile.cmake b/res/vcpkg/ffmpeg/portfile.cmake index 3d4c10906dfa..d56475c059f8 100644 --- a/res/vcpkg/ffmpeg/portfile.cmake +++ b/res/vcpkg/ffmpeg/portfile.cmake @@ -13,6 +13,8 @@ vcpkg_from_github( patch/0001-avcodec-amfenc-add-query_timeout-option-for-h264-hev.patch patch/0002-libavcodec-amfenc-reconfig-when-bitrate-change.patch patch/0003-amf-colorspace.patch + patch/0004-videotoolbox-changing-bitrate.patch + patch/0005-mediacodec-changing-bitrate.patch ) if(SOURCE_PATH MATCHES " ") From 9d9b67aca58e35e95ff08fb0686b3494a3cedfc7 Mon Sep 17 00:00:00 2001 From: 21pages Date: Sat, 30 Nov 2024 12:19:42 +0800 Subject: [PATCH 429/541] update flutter texture rgba renderer plugin, remove switch rgba (#10070) Signed-off-by: 21pages --- flutter/lib/models/model.dart | 4 +++- flutter/pubspec.lock | 9 +++++---- flutter/pubspec.yaml | 5 ++++- src/client.rs | 8 +++++++- 4 files changed, 19 insertions(+), 7 deletions(-) diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index fdcd28f8d04c..e5cde939fb76 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -1268,7 +1268,9 @@ class ImageModel with ChangeNotifier { rgba, rect?.width.toInt() ?? 0, rect?.height.toInt() ?? 0, - isWeb ? ui.PixelFormat.rgba8888 : ui.PixelFormat.bgra8888, + isWeb | isWindows | isLinux + ? ui.PixelFormat.rgba8888 + : ui.PixelFormat.bgra8888, ); if (parent.target?.id != pid) return; await update(image); diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index 7c60e037a57d..31f04ce4076d 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -1277,10 +1277,11 @@ packages: texture_rgba_renderer: dependency: "direct main" description: - name: texture_rgba_renderer - sha256: cb048abdd800468ca40749ca10d1db9d1e6a055d1cde6234c05191293f0c7d61 - url: "https://pub.dev" - source: hosted + path: "." + ref: "42797e0f03141dc2b585f76c64a13974508058b4" + resolved-ref: "42797e0f03141dc2b585f76c64a13974508058b4" + url: "https://github.com/rustdesk-org/flutter_texture_rgba_renderer" + source: git version: "0.0.16" timing: dependency: transitive diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index afe09a0dc72e..bec58a4d6ced 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -91,7 +91,10 @@ dependencies: password_strength: ^0.2.0 flutter_launcher_icons: ^0.13.1 flutter_keyboard_visibility: ^5.4.0 - texture_rgba_renderer: ^0.0.16 + texture_rgba_renderer: + git: + url: https://github.com/rustdesk-org/flutter_texture_rgba_renderer + ref: 42797e0f03141dc2b585f76c64a13974508058b4 percent_indicator: ^4.2.2 dropdown_button2: ^2.0.0 flutter_gpu_texture_renderer: diff --git a/src/client.rs b/src/client.rs index 474c7fdfc0b8..4ff2c6b522b6 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1188,9 +1188,15 @@ impl VideoHandler { pub fn new(format: CodecFormat, _display: usize) -> Self { let luid = Self::get_adapter_luid(); log::info!("new video handler for display #{_display}, format: {format:?}, luid: {luid:?}"); + let rgba_format = + if cfg!(feature = "flutter") && (cfg!(windows) || cfg!(target_os = "linux")) { + ImageFormat::ABGR + } else { + ImageFormat::ARGB + }; VideoHandler { decoder: Decoder::new(format, luid), - rgb: ImageRgb::new(ImageFormat::ARGB, crate::get_dst_align_rgba()), + rgb: ImageRgb::new(rgba_format, crate::get_dst_align_rgba()), texture: Default::default(), recorder: Default::default(), record: false, From 743b0ce8ce8537ee54e283af6753b1c0dcbcc344 Mon Sep 17 00:00:00 2001 From: 21pages Date: Sat, 30 Nov 2024 13:29:03 +0800 Subject: [PATCH 430/541] fix mediacodec patch (#10119) ensure set_parameters_id is not null Signed-off-by: 21pages --- .../0005-mediacodec-changing-bitrate.patch | 31 +++++++++++-------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/res/vcpkg/ffmpeg/patch/0005-mediacodec-changing-bitrate.patch b/res/vcpkg/ffmpeg/patch/0005-mediacodec-changing-bitrate.patch index 90c3613a43b0..1f70a5659308 100644 --- a/res/vcpkg/ffmpeg/patch/0005-mediacodec-changing-bitrate.patch +++ b/res/vcpkg/ffmpeg/patch/0005-mediacodec-changing-bitrate.patch @@ -1,17 +1,17 @@ -From fb5cc7909a9b288f6bd13c75992b66ed257ab019 Mon Sep 17 00:00:00 2001 +From 51ac90d8084f7b153eac5133765fa9d0365aa239 Mon Sep 17 00:00:00 2001 From: 21pages Date: Sun, 24 Nov 2024 14:17:39 +0800 -Subject: [PATCH 2/2] mediacodec changing bitrate +Subject: [PATCH 1/4] mediacodec changing bitrate Signed-off-by: 21pages --- - libavcodec/mediacodec_wrapper.c | 96 +++++++++++++++++++++++++++++++++ - libavcodec/mediacodec_wrapper.h | 7 +++ - libavcodec/mediacodecenc.c | 18 +++++++ - 3 files changed, 121 insertions(+) + libavcodec/mediacodec_wrapper.c | 101 ++++++++++++++++++++++++++++++++ + libavcodec/mediacodec_wrapper.h | 7 +++ + libavcodec/mediacodecenc.c | 18 ++++++ + 3 files changed, 126 insertions(+) diff --git a/libavcodec/mediacodec_wrapper.c b/libavcodec/mediacodec_wrapper.c -index 306359071e..44fdd71869 100644 +index 306359071e..1ab4e673f6 100644 --- a/libavcodec/mediacodec_wrapper.c +++ b/libavcodec/mediacodec_wrapper.c @@ -35,6 +35,8 @@ @@ -66,7 +66,7 @@ index 306359071e..44fdd71869 100644 #define JNI_GET_ENV_OR_RETURN(env, log_ctx, ret) do { \ (env) = ff_jni_get_env(log_ctx); \ if (!(env)) { \ -@@ -1761,6 +1785,64 @@ static int mediacodec_jni_signalEndOfInputStream(FFAMediaCodec *ctx) +@@ -1761,6 +1785,69 @@ static int mediacodec_jni_signalEndOfInputStream(FFAMediaCodec *ctx) return 0; } @@ -104,6 +104,11 @@ index 306359071e..44fdd71869 100644 + goto fail; + } + ++ if (!codec->jfields.set_parameters_id) { ++ av_log(log_ctx, AV_LOG_ERROR, "System doesn't support setParameters\n"); ++ goto fail; ++ } ++ + (*env)->CallVoidMethod(env, codec->object, codec->jfields.set_parameters_id, object); + if (ff_jni_exception_check(env, 1, log_ctx) < 0) { + goto fail; @@ -131,7 +136,7 @@ index 306359071e..44fdd71869 100644 static const FFAMediaFormat media_format_jni = { .class = &amediaformat_class, -@@ -1820,6 +1902,8 @@ static const FFAMediaCodec media_codec_jni = { +@@ -1820,6 +1907,8 @@ static const FFAMediaCodec media_codec_jni = { .getConfigureFlagEncode = mediacodec_jni_getConfigureFlagEncode, .cleanOutputBuffers = mediacodec_jni_cleanOutputBuffers, .signalEndOfInputStream = mediacodec_jni_signalEndOfInputStream, @@ -140,7 +145,7 @@ index 306359071e..44fdd71869 100644 }; typedef struct FFAMediaFormatNdk { -@@ -1893,6 +1977,8 @@ typedef struct FFAMediaCodecNdk { +@@ -1893,6 +1982,8 @@ typedef struct FFAMediaCodecNdk { // Available since API level 26. media_status_t (*setInputSurface)(AMediaCodec*, ANativeWindow *); media_status_t (*signalEndOfInputStream)(AMediaCodec *); @@ -149,7 +154,7 @@ index 306359071e..44fdd71869 100644 } FFAMediaCodecNdk; static const FFAMediaFormat media_format_ndk; -@@ -2154,6 +2240,8 @@ static inline FFAMediaCodec *ndk_codec_create(int method, const char *arg) { +@@ -2154,6 +2245,8 @@ static inline FFAMediaCodec *ndk_codec_create(int method, const char *arg) { GET_SYMBOL(setInputSurface, 0) GET_SYMBOL(signalEndOfInputStream, 0) @@ -158,7 +163,7 @@ index 306359071e..44fdd71869 100644 #undef GET_SYMBOL switch (method) { -@@ -2428,6 +2516,12 @@ static int mediacodec_ndk_signalEndOfInputStream(FFAMediaCodec *ctx) +@@ -2428,6 +2521,12 @@ static int mediacodec_ndk_signalEndOfInputStream(FFAMediaCodec *ctx) return 0; } @@ -171,7 +176,7 @@ index 306359071e..44fdd71869 100644 static const FFAMediaFormat media_format_ndk = { .class = &amediaformat_ndk_class, -@@ -2489,6 +2583,8 @@ static const FFAMediaCodec media_codec_ndk = { +@@ -2489,6 +2588,8 @@ static const FFAMediaCodec media_codec_ndk = { .getConfigureFlagEncode = mediacodec_ndk_getConfigureFlagEncode, .cleanOutputBuffers = mediacodec_ndk_cleanOutputBuffers, .signalEndOfInputStream = mediacodec_ndk_signalEndOfInputStream, From 082a66b28237035ab30f21081daa03e8acda1ad9 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Sat, 30 Nov 2024 15:01:44 +0800 Subject: [PATCH 431/541] refact: remove flutter_improved_scrolling (#10120) Signed-off-by: fufesou --- flutter/lib/common.dart | 24 -- flutter/lib/common/widgets/peers_view.dart | 44 +-- flutter/lib/consts.dart | 4 - .../lib/desktop/pages/desktop_home_page.dart | 71 ++-- .../desktop/pages/desktop_setting_page.dart | 324 ++++++++---------- flutter/lib/desktop/pages/remote_page.dart | 51 +-- .../lib/desktop/widgets/scroll_wrapper.dart | 27 -- flutter/pubspec.lock | 9 - flutter/pubspec.yaml | 7 - 9 files changed, 213 insertions(+), 348 deletions(-) delete mode 100644 flutter/lib/desktop/widgets/scroll_wrapper.dart diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index fb389b45e646..4e97449124f7 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -2730,30 +2730,6 @@ Future osxRequestAudio() async { return await kMacOSPermChannel.invokeMethod("requestRecordAudio"); } -class DraggableNeverScrollableScrollPhysics extends ScrollPhysics { - /// Creates scroll physics that does not let the user scroll. - const DraggableNeverScrollableScrollPhysics({super.parent}); - - @override - DraggableNeverScrollableScrollPhysics applyTo(ScrollPhysics? ancestor) { - return DraggableNeverScrollableScrollPhysics(parent: buildParent(ancestor)); - } - - @override - bool shouldAcceptUserOffset(ScrollMetrics position) { - // TODO: find a better solution to check if the offset change is caused by the scrollbar. - // Workaround: when dragging with the scrollbar, it always triggers an [IdleScrollActivity]. - if (position is ScrollPositionWithSingleContext) { - // ignore: invalid_use_of_protected_member, invalid_use_of_visible_for_testing_member - return position.activity is IdleScrollActivity; - } - return false; - } - - @override - bool get allowImplicitScrolling => false; -} - Widget futureBuilder( {required Future? future, required Widget Function(dynamic data) hasData}) { return FutureBuilder( diff --git a/flutter/lib/common/widgets/peers_view.dart b/flutter/lib/common/widgets/peers_view.dart index e14e198bd105..3e34f882d1de 100644 --- a/flutter/lib/common/widgets/peers_view.dart +++ b/flutter/lib/common/widgets/peers_view.dart @@ -5,7 +5,6 @@ import 'package:dynamic_layouts/dynamic_layouts.dart'; import 'package:flutter/foundation.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/models/ab_model.dart'; import 'package:flutter_hbb/models/peer_tab_model.dart'; import 'package:flutter_hbb/models/state_model.dart'; @@ -271,33 +270,24 @@ class _PeersViewState extends State<_PeersView> }, ) : 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); - }), + ? ListView.builder( + controller: _scrollController, + 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); - }), - )); + : DynamicGridView.builder( + gridDelegate: SliverGridDelegateWithWrapping( + mainAxisSpacing: space / 2, + crossAxisSpacing: space), + itemCount: peers.length, + itemBuilder: (BuildContext context, int index) { + return buildOnePeer(peers[index], false); + })); if (updateEvent == UpdateEvent.load) { _curPeers.clear(); diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index c313958bddfc..f6f9c4d34f9a 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -244,10 +244,6 @@ const double kDesktopIconButtonSplashRadius = 20; /// [kMinCursorSize] indicates min cursor (w, h) const int kMinCursorSize = 12; -/// [kDefaultScrollAmountMultiplier] indicates how many rows can be scrolled after a minimum scroll action of mouse -const kDefaultScrollAmountMultiplier = 5.0; -const kDefaultScrollDuration = Duration(milliseconds: 50); -const kDefaultMouseWheelThrottleDuration = Duration(milliseconds: 50); const kFullScreenEdgeSize = 0.0; const kMaximizeEdgeSize = 0.0; // Do not use kWindowResizeEdgeSize directly. Use `windowResizeEdgeSize` in `common.dart` instead. diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index 9728d6b478e2..04a186b84c63 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -12,7 +12,6 @@ import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/desktop/pages/connection_page.dart'; import 'package:flutter_hbb/desktop/pages/desktop_setting_page.dart'; import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart'; -import 'package:flutter_hbb/desktop/widgets/scroll_wrapper.dart'; import 'package:flutter_hbb/models/platform_model.dart'; import 'package:flutter_hbb/models/server_model.dart'; import 'package:flutter_hbb/plugin/ui_manager.dart'; @@ -125,47 +124,43 @@ class _DesktopHomePageState extends State child: Container( width: isIncomingOnly ? 280.0 : 200.0, color: Theme.of(context).colorScheme.background, - child: DesktopScrollWrapper( - scrollController: _leftPaneScrollController, - child: Stack( - children: [ - SingleChildScrollView( - controller: _leftPaneScrollController, - physics: DraggableNeverScrollableScrollPhysics(), - child: Column( - key: _childKey, - children: children, - ), + child: Stack( + children: [ + SingleChildScrollView( + controller: _leftPaneScrollController, + child: Column( + key: _childKey, + children: children, ), - 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, - ), + ), + 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, ), + onTap: () => { + if (DesktopSettingPage.tabKeys.isNotEmpty) + { + DesktopSettingPage.switch2page( + DesktopSettingPage.tabKeys[0]) + } + }, + onHover: (value) => _editHover.value = value, ), - ) - ], - ), + ), + ) + ], ), ), ); diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 69100470f0ea..0c7586e7036c 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -19,7 +19,6 @@ import 'package:get/get.dart'; import 'package:provider/provider.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher_string.dart'; -import 'package:flutter_hbb/desktop/widgets/scroll_wrapper.dart'; import '../../common/widgets/dialog.dart'; import '../../common/widgets/login.dart'; @@ -226,13 +225,11 @@ class _DesktopSettingPageState extends State Expanded( child: Container( color: Theme.of(context).scaffoldBackgroundColor, - child: DesktopScrollWrapper( - scrollController: controller, - child: PageView( - controller: controller, - physics: NeverScrollableScrollPhysics(), - children: _children(), - )), + child: PageView( + controller: controller, + physics: NeverScrollableScrollPhysics(), + children: _children(), + ), ), ) ], @@ -281,13 +278,10 @@ class _DesktopSettingPageState extends State Widget _listView({required List<_TabInfo> tabs}) { final scrollController = ScrollController(); - return DesktopScrollWrapper( - scrollController: scrollController, - child: ListView( - physics: DraggableNeverScrollableScrollPhysics(), - controller: scrollController, - children: tabs.map((tab) => _listItem(tab: tab)).toList(), - )); + return ListView( + controller: scrollController, + children: tabs.map((tab) => _listItem(tab: tab)).toList(), + ); } Widget _listItem({required _TabInfo tab}) { @@ -349,22 +343,19 @@ class _GeneralState extends State<_General> { @override Widget build(BuildContext context) { final scrollController = ScrollController(); - return DesktopScrollWrapper( - scrollController: scrollController, - child: ListView( - physics: DraggableNeverScrollableScrollPhysics(), - controller: scrollController, - children: [ - if (!isWeb) service(), - theme(), - _Card(title: 'Language', children: [language()]), - if (!isWeb) hwcodec(), - if (!isWeb) audio(context), - if (!isWeb) record(context), - if (!isWeb) WaylandCard(), - other() - ], - ).marginOnly(bottom: _kListViewBottomMargin)); + return ListView( + controller: scrollController, + children: [ + if (!isWeb) service(), + theme(), + _Card(title: 'Language', children: [language()]), + if (!isWeb) hwcodec(), + if (!isWeb) audio(context), + if (!isWeb) record(context), + if (!isWeb) WaylandCard(), + other() + ], + ).marginOnly(bottom: _kListViewBottomMargin); } Widget theme() { @@ -705,29 +696,26 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { @override Widget build(BuildContext context) { super.build(context); - return DesktopScrollWrapper( - scrollController: scrollController, - child: SingleChildScrollView( - physics: DraggableNeverScrollableScrollPhysics(), - controller: scrollController, - child: Column( - children: [ - _lock(locked, 'Unlock Security Settings', () { - locked = false; - setState(() => {}); - }), - AbsorbPointer( - absorbing: locked, - child: Column(children: [ - permissions(context), - password(context), - _Card(title: '2FA', children: [tfa()]), - _Card(title: 'ID', children: [changeId()]), - more(context), - ]), - ), - ], - )).marginOnly(bottom: _kListViewBottomMargin)); + return SingleChildScrollView( + controller: scrollController, + child: Column( + children: [ + _lock(locked, 'Unlock Security Settings', () { + locked = false; + setState(() => {}); + }), + AbsorbPointer( + absorbing: locked, + child: Column(children: [ + permissions(context), + password(context), + _Card(title: '2FA', children: [tfa()]), + _Card(title: 'ID', children: [changeId()]), + more(context), + ]), + ), + ], + )).marginOnly(bottom: _kListViewBottomMargin); } Widget tfa() { @@ -1384,28 +1372,23 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin { // TODO: support web proxy final hideProxy = isWeb || bind.mainGetBuildinOption(key: kOptionHideProxySetting) == 'Y'; - return DesktopScrollWrapper( - scrollController: scrollController, - child: ListView( - controller: scrollController, - physics: DraggableNeverScrollableScrollPhysics(), - children: [ - _lock(locked, 'Unlock Network Settings', () { - locked = false; - setState(() => {}); - }), - AbsorbPointer( - absorbing: locked, - child: Column(children: [ - if (!hideServer) server(enabled), - if (!hideProxy) - _Card(title: 'Proxy', children: [ - _Button('Socks5/Http(s) Proxy', changeSocks5Proxy, - enabled: enabled), - ]), - ]), - ), - ]).marginOnly(bottom: _kListViewBottomMargin)); + return ListView(controller: scrollController, children: [ + _lock(locked, 'Unlock Network Settings', () { + locked = false; + setState(() => {}); + }), + AbsorbPointer( + absorbing: locked, + child: Column(children: [ + if (!hideServer) server(enabled), + if (!hideProxy) + _Card(title: 'Proxy', children: [ + _Button('Socks5/Http(s) Proxy', changeSocks5Proxy, + enabled: enabled), + ]), + ]), + ), + ]).marginOnly(bottom: _kListViewBottomMargin); } server(bool enabled) { @@ -1494,19 +1477,14 @@ class _DisplayState extends State<_Display> { @override Widget build(BuildContext context) { final scrollController = ScrollController(); - return DesktopScrollWrapper( - scrollController: scrollController, - child: ListView( - controller: scrollController, - physics: DraggableNeverScrollableScrollPhysics(), - children: [ - viewStyle(context), - scrollStyle(context), - imageQuality(context), - codec(context), - if (!isWeb) privacyModeImpl(context), - other(context), - ]).marginOnly(bottom: _kListViewBottomMargin)); + return ListView(controller: scrollController, children: [ + viewStyle(context), + scrollStyle(context), + imageQuality(context), + codec(context), + if (!isWeb) privacyModeImpl(context), + other(context), + ]).marginOnly(bottom: _kListViewBottomMargin); } Widget viewStyle(BuildContext context) { @@ -1729,15 +1707,12 @@ class _AccountState extends State<_Account> { @override Widget build(BuildContext context) { final scrollController = ScrollController(); - return DesktopScrollWrapper( - scrollController: scrollController, - child: ListView( - physics: DraggableNeverScrollableScrollPhysics(), - controller: scrollController, - children: [ - _Card(title: 'Account', children: [accountAction(), useInfo()]), - ], - ).marginOnly(bottom: _kListViewBottomMargin)); + return ListView( + controller: scrollController, + children: [ + _Card(title: 'Account', children: [accountAction(), useInfo()]), + ], + ).marginOnly(bottom: _kListViewBottomMargin); } Widget accountAction() { @@ -1834,18 +1809,14 @@ class _PluginState extends State<_Plugin> { Widget build(BuildContext context) { bind.pluginListReload(); final scrollController = ScrollController(); - return DesktopScrollWrapper( - scrollController: scrollController, - child: ChangeNotifierProvider.value( - value: pluginManager, - child: Consumer(builder: (context, model, child) { - return ListView( - physics: DraggableNeverScrollableScrollPhysics(), - controller: scrollController, - children: model.plugins.map((entry) => pluginCard(entry)).toList(), - ).marginOnly(bottom: _kListViewBottomMargin); - }), - ), + return ChangeNotifierProvider.value( + value: pluginManager, + child: Consumer(builder: (context, model, child) { + return ListView( + controller: scrollController, + children: model.plugins.map((entry) => pluginCard(entry)).toList(), + ).marginOnly(bottom: _kListViewBottomMargin); + }), ); } @@ -1897,75 +1868,72 @@ class _AboutState extends State<_About> { final fingerprint = data['fingerprint'].toString(); const linkStyle = TextStyle(decoration: TextDecoration.underline); final scrollController = ScrollController(); - return DesktopScrollWrapper( - scrollController: scrollController, - child: SingleChildScrollView( - controller: scrollController, - physics: DraggableNeverScrollableScrollPhysics(), - child: _Card(title: translate('About RustDesk'), children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox( - height: 8.0, - ), - SelectionArea( - child: Text('${translate('Version')}: $version') - .marginSymmetric(vertical: 4.0)), - SelectionArea( - child: Text('${translate('Build Date')}: $buildDate') - .marginSymmetric(vertical: 4.0)), - if (!isWeb) - SelectionArea( - child: Text('${translate('Fingerprint')}: $fingerprint') - .marginSymmetric(vertical: 4.0)), - InkWell( - onTap: () { - launchUrlString('https://rustdesk.com/privacy.html'); - }, - child: Text( - translate('Privacy Statement'), - style: linkStyle, - ).marginSymmetric(vertical: 4.0)), - InkWell( - onTap: () { - launchUrlString('https://rustdesk.com'); - }, - child: Text( - translate('Website'), - style: linkStyle, - ).marginSymmetric(vertical: 4.0)), - Container( - decoration: const BoxDecoration(color: Color(0xFF2c8cff)), - padding: - const EdgeInsets.symmetric(vertical: 24, horizontal: 8), - child: SelectionArea( - child: Row( - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Copyright © ${DateTime.now().toString().substring(0, 4)} Purslane Ltd.\n$license', - style: const TextStyle(color: Colors.white), - ), - Text( - translate('Slogan_tip'), - style: TextStyle( - fontWeight: FontWeight.w800, - color: Colors.white), - ) - ], + return SingleChildScrollView( + controller: scrollController, + child: _Card(title: translate('About RustDesk'), children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox( + height: 8.0, + ), + SelectionArea( + child: Text('${translate('Version')}: $version') + .marginSymmetric(vertical: 4.0)), + SelectionArea( + child: Text('${translate('Build Date')}: $buildDate') + .marginSymmetric(vertical: 4.0)), + if (!isWeb) + SelectionArea( + child: Text('${translate('Fingerprint')}: $fingerprint') + .marginSymmetric(vertical: 4.0)), + InkWell( + onTap: () { + launchUrlString('https://rustdesk.com/privacy.html'); + }, + child: Text( + translate('Privacy Statement'), + style: linkStyle, + ).marginSymmetric(vertical: 4.0)), + InkWell( + onTap: () { + launchUrlString('https://rustdesk.com'); + }, + child: Text( + translate('Website'), + style: linkStyle, + ).marginSymmetric(vertical: 4.0)), + Container( + decoration: const BoxDecoration(color: Color(0xFF2c8cff)), + padding: + const EdgeInsets.symmetric(vertical: 24, horizontal: 8), + child: SelectionArea( + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Copyright © ${DateTime.now().toString().substring(0, 4)} Purslane Ltd.\n$license', + style: const TextStyle(color: Colors.white), ), - ), - ], - )), - ).marginSymmetric(vertical: 4.0) - ], - ).marginOnly(left: _kContentHMargin) - ]), - )); + Text( + translate('Slogan_tip'), + style: TextStyle( + fontWeight: FontWeight.w800, + color: Colors.white), + ) + ], + ), + ), + ], + )), + ).marginSymmetric(vertical: 4.0) + ], + ).marginOnly(left: _kContentHMargin) + ]), + ); }); } } diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index cca2074a242c..912b06b0282b 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -6,7 +6,6 @@ import 'package:flutter/services.dart'; import 'package:get/get.dart'; import 'package:provider/provider.dart'; import 'package:wakelock_plus/wakelock_plus.dart'; -import 'package:flutter_improved_scrolling/flutter_improved_scrolling.dart'; import 'package:flutter_hbb/models/state_model.dart'; import '../../consts.dart'; @@ -742,12 +741,6 @@ class _ImagePaintState extends State { ScrollController horizontal, ScrollController vertical, ) { - final scrollConfig = CustomMouseWheelScrollConfig( - scrollDuration: kDefaultScrollDuration, - scrollCurve: Curves.linearToEaseOut, - mouseWheelTurnsThrottleTimeMs: - kDefaultMouseWheelThrottleDuration.inMilliseconds, - scrollAmountMultiplier: kDefaultScrollAmountMultiplier); var widget = child; if (layoutSize.width < size.width) { widget = ScrollConfiguration( @@ -793,36 +786,26 @@ class _ImagePaintState extends State { ); } if (layoutSize.width < size.width) { - widget = ImprovedScrolling( - scrollController: horizontal, - enableCustomMouseWheelScrolling: cursorOverImage.isFalse, - customMouseWheelScrollConfig: scrollConfig, - child: RawScrollbar( - thickness: kScrollbarThickness, - thumbColor: Colors.grey, - controller: horizontal, - thumbVisibility: false, - trackVisibility: false, - notificationPredicate: layoutSize.height < size.height - ? (notification) => notification.depth == 1 - : defaultScrollNotificationPredicate, - child: widget, - ), + widget = RawScrollbar( + thickness: kScrollbarThickness, + thumbColor: Colors.grey, + controller: horizontal, + thumbVisibility: false, + trackVisibility: false, + notificationPredicate: layoutSize.height < size.height + ? (notification) => notification.depth == 1 + : defaultScrollNotificationPredicate, + child: widget, ); } if (layoutSize.height < size.height) { - widget = ImprovedScrolling( - scrollController: vertical, - enableCustomMouseWheelScrolling: cursorOverImage.isFalse, - customMouseWheelScrollConfig: scrollConfig, - child: RawScrollbar( - thickness: kScrollbarThickness, - thumbColor: Colors.grey, - controller: vertical, - thumbVisibility: false, - trackVisibility: false, - child: widget, - ), + widget = RawScrollbar( + thickness: kScrollbarThickness, + thumbColor: Colors.grey, + controller: vertical, + thumbVisibility: false, + trackVisibility: false, + child: widget, ); } diff --git a/flutter/lib/desktop/widgets/scroll_wrapper.dart b/flutter/lib/desktop/widgets/scroll_wrapper.dart deleted file mode 100644 index c5bc3394b439..000000000000 --- a/flutter/lib/desktop/widgets/scroll_wrapper.dart +++ /dev/null @@ -1,27 +0,0 @@ -import 'package:flutter/widgets.dart'; -import 'package:flutter_hbb/consts.dart'; -import 'package:flutter_improved_scrolling/flutter_improved_scrolling.dart'; - -class DesktopScrollWrapper extends StatelessWidget { - final ScrollController scrollController; - final Widget child; - const DesktopScrollWrapper( - {Key? key, required this.scrollController, required this.child}) - : super(key: key); - - @override - Widget build(BuildContext context) { - return ImprovedScrolling( - scrollController: scrollController, - enableCustomMouseWheelScrolling: true, - // enableKeyboardScrolling: true, // strange behavior on mac - customMouseWheelScrollConfig: CustomMouseWheelScrollConfig( - scrollDuration: kDefaultScrollDuration, - scrollCurve: Curves.linearToEaseOut, - mouseWheelTurnsThrottleTimeMs: - kDefaultMouseWheelThrottleDuration.inMilliseconds, - scrollAmountMultiplier: kDefaultScrollAmountMultiplier), - child: child, - ); - } -} diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index 31f04ce4076d..98f3eef96353 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -530,15 +530,6 @@ packages: url: "https://github.com/rustdesk-org/flutter_gpu_texture_renderer" source: git version: "0.0.1" - flutter_improved_scrolling: - dependency: "direct main" - description: - path: "." - ref: HEAD - resolved-ref: "62f09545149f320616467c306c8c5f71714a18e6" - url: "https://github.com/rustdesk-org/flutter_improved_scrolling" - source: git - version: "0.0.3" flutter_keyboard_visibility: dependency: "direct main" description: diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index bec58a4d6ced..3a4f46b2a36f 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -71,13 +71,6 @@ dependencies: debounce_throttle: ^2.0.0 file_picker: ^5.1.0 flutter_svg: ^2.0.5 - flutter_improved_scrolling: - # currently, we use flutter 3.10.0+. - # - # for flutter 3.0.5, please use official version(just comment code below). - # if build rustdesk by flutter >=3.3, please use our custom pub below (uncomment code below). - git: - url: https://github.com/rustdesk-org/flutter_improved_scrolling uni_links: git: url: https://github.com/rustdesk-org/uni_links From f8c2713c5bc945f0758026c33558515d5e7a4719 Mon Sep 17 00:00:00 2001 From: Kleofass <4000163+Kleofass@users.noreply.github.com> Date: Sat, 30 Nov 2024 18:27:39 +0200 Subject: [PATCH 432/541] Update lv.rs (#10124) --- src/lang/lv.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lang/lv.rs b/src/lang/lv.rs index 166830c16b4b..31a8d89c02c8 100644 --- a/src/lang/lv.rs +++ b/src/lang/lv.rs @@ -653,7 +653,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", "Augšupielādēt mapi"), ("Upload files", "Augšupielādēt failus"), ("Clipboard is synchronized", "Starpliktuve ir sinhronizēta"), - ("Update client clipboard", ""), - ("Untagged", ""), + ("Update client clipboard", "Atjaunināt klienta starpliktuvi"), + ("Untagged", "Neatzīmēts"), ].iter().cloned().collect(); } From 8d4c86fe7ff96e38c526c910e8381d697ab66446 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Sun, 1 Dec 2024 17:19:21 +0800 Subject: [PATCH 433/541] fix: workaround, linux window, transparent rounded corner (#10128) * fix: linux window, rounded corner Signed-off-by: fufesou * Update my_application.cc --------- Signed-off-by: fufesou Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com> --- flutter/linux/my_application.cc | 45 +++++++++++++++++++++++++++++++-- flutter/pubspec.lock | 2 +- 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/flutter/linux/my_application.cc b/flutter/linux/my_application.cc index 56b85ccae6dd..f4247bd94613 100644 --- a/flutter/linux/my_application.cc +++ b/flutter/linux/my_application.cc @@ -16,6 +16,8 @@ G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) extern bool gIsConnectionManager; +GtkWidget *find_gl_area(GtkWidget *widget); + // Implements GApplication::activate. static void my_application_activate(GApplication* application) { MyApplication* self = MY_APPLICATION(application); @@ -39,9 +41,10 @@ static void my_application_activate(GApplication* application) { // If running on Wayland assume the header bar will work (may need changing // if future cases occur). gboolean use_header_bar = TRUE; + GdkScreen* screen = NULL; #ifdef GDK_WINDOWING_X11 - GdkScreen* screen = gtk_window_get_screen(window); - if (GDK_IS_X11_SCREEN(screen)) { + screen = gtk_window_get_screen(window); + if (screen != NULL && GDK_IS_X11_SCREEN(screen)) { const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); if (g_strcmp0(wm_name, "GNOME Shell") != 0) { use_header_bar = FALSE; @@ -76,6 +79,22 @@ static void my_application_activate(GApplication* application) { gtk_widget_show(GTK_WIDGET(view)); gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); + // https://github.com/flutter/flutter/issues/152154 + // Remove this workaround when flutter version is updated. + GtkWidget *gl_area = find_gl_area(GTK_WIDGET(view)); + if (gl_area != NULL) { + gtk_gl_area_set_has_alpha(GTK_GL_AREA(gl_area), TRUE); + } + + if (screen != NULL) { + GdkVisual *visual = NULL; + gtk_widget_set_app_paintable(GTK_WIDGET(window), TRUE); + visual = gdk_screen_get_rgba_visual(screen); + if (visual != NULL && gdk_screen_is_composited(screen)) { + gtk_widget_set_visual(GTK_WIDGET(window), visual); + } + } + fl_register_plugins(FL_PLUGIN_REGISTRY(view)); gtk_widget_grab_focus(GTK_WIDGET(view)); @@ -121,3 +140,25 @@ MyApplication* my_application_new() { "flags", G_APPLICATION_NON_UNIQUE, nullptr)); } + +GtkWidget *find_gl_area(GtkWidget *widget) +{ + if (GTK_IS_GL_AREA(widget)) { + return widget; + } + + if (GTK_IS_CONTAINER(widget)) { + GList *children = gtk_container_get_children(GTK_CONTAINER(widget)); + for (GList *iter = children; iter != NULL; iter = g_list_next(iter)) { + GtkWidget *child = GTK_WIDGET(iter->data); + GtkWidget *gl_area = find_gl_area(child); + if (gl_area != NULL) { + g_list_free(children); + return gl_area; + } + } + g_list_free(children); + } + + return NULL; +} diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index 98f3eef96353..58df59a5521f 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -335,7 +335,7 @@ packages: description: path: "." ref: HEAD - resolved-ref: "519350f1f40746798299e94786197d058353bac9" + resolved-ref: "4f562ab49d289cfa36bfda7cff12746ec0200033" url: "https://github.com/rustdesk-org/rustdesk_desktop_multi_window" source: git version: "0.1.0" From f330953f4f59cbfb3bca08b3466a7188152bdb85 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Sun, 1 Dec 2024 18:49:24 +0800 Subject: [PATCH 434/541] bump to 1.3.4 --- .github/workflows/flutter-build.yml | 4 ++-- .github/workflows/playground.yml | 2 +- Cargo.lock | 4 ++-- Cargo.toml | 2 +- appimage/AppImageBuilder-aarch64.yml | 2 +- appimage/AppImageBuilder-x86_64.yml | 2 +- flutter/pubspec.yaml | 2 +- libs/portable/Cargo.toml | 2 +- res/PKGBUILD | 2 +- res/rpm-flutter-suse.spec | 2 +- res/rpm-flutter.spec | 2 +- res/rpm.spec | 2 +- 12 files changed, 14 insertions(+), 14 deletions(-) diff --git a/.github/workflows/flutter-build.yml b/.github/workflows/flutter-build.yml index b295e70f6f87..5ac769ea65df 100644 --- a/.github/workflows/flutter-build.yml +++ b/.github/workflows/flutter-build.yml @@ -33,7 +33,7 @@ env: VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite" # vcpkg version: 2024.07.12 VCPKG_COMMIT_ID: "1de2026f28ead93ff1773e6e680387643e914ea1" - VERSION: "1.3.3" + VERSION: "1.3.4" NDK_VERSION: "r27c" #signing keys env variable checks ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}" @@ -714,7 +714,7 @@ jobs: shell: bash run: | cd "$(dirname "$(which flutter)")" - # https://github.com/flutter/flutter/issues/133533 + # https://github.com/flutter/flutter/issues/1.3.43 sed -i -e 's/_setFramesEnabledState(false);/\/\/_setFramesEnabledState(false);/g' ../packages/flutter/lib/src/scheduler/binding.dart grep -n '_setFramesEnabledState(false);' ../packages/flutter/lib/src/scheduler/binding.dart diff --git a/.github/workflows/playground.yml b/.github/workflows/playground.yml index 9d4d42cb3eda..282f678e8304 100644 --- a/.github/workflows/playground.yml +++ b/.github/workflows/playground.yml @@ -18,7 +18,7 @@ env: VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite" # vcpkg version: 2024.06.15 VCPKG_COMMIT_ID: "f7423ee180c4b7f40d43402c2feb3859161ef625" - VERSION: "1.3.3" + VERSION: "1.3.4" NDK_VERSION: "r26d" #signing keys env variable checks ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}" diff --git a/Cargo.lock b/Cargo.lock index 6e2e56b3bcb5..62f612397b98 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5494,7 +5494,7 @@ dependencies = [ [[package]] name = "rustdesk" -version = "1.3.3" +version = "1.3.4" dependencies = [ "android-wakelock", "android_logger", @@ -5594,7 +5594,7 @@ dependencies = [ [[package]] name = "rustdesk-portable-packer" -version = "1.3.3" +version = "1.3.4" dependencies = [ "brotli", "dirs 5.0.1", diff --git a/Cargo.toml b/Cargo.toml index dbf819bf8389..3cc05b780551 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rustdesk" -version = "1.3.3" +version = "1.3.4" authors = ["rustdesk "] edition = "2021" build= "build.rs" diff --git a/appimage/AppImageBuilder-aarch64.yml b/appimage/AppImageBuilder-aarch64.yml index a6bd632dc0cb..65a73ee52b68 100644 --- a/appimage/AppImageBuilder-aarch64.yml +++ b/appimage/AppImageBuilder-aarch64.yml @@ -18,7 +18,7 @@ AppDir: id: rustdesk name: rustdesk icon: rustdesk - version: 1.3.3 + version: 1.3.4 exec: usr/lib/rustdesk/rustdesk exec_args: $@ apt: diff --git a/appimage/AppImageBuilder-x86_64.yml b/appimage/AppImageBuilder-x86_64.yml index 9ea820fec475..430400721a8b 100644 --- a/appimage/AppImageBuilder-x86_64.yml +++ b/appimage/AppImageBuilder-x86_64.yml @@ -18,7 +18,7 @@ AppDir: id: rustdesk name: rustdesk icon: rustdesk - version: 1.3.3 + version: 1.3.4 exec: usr/lib/rustdesk/rustdesk exec_args: $@ apt: diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index 3a4f46b2a36f..88e9dda1b874 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # 1.1.9-1 works for android, but for ios it becomes 1.1.91, need to set it to 1.1.9-a.1 for iOS, will get 1.1.9.1, but iOS store not allow 4 numbers -version: 1.3.3+52 +version: 1.3.4+53 environment: sdk: '^3.1.0' diff --git a/libs/portable/Cargo.toml b/libs/portable/Cargo.toml index 3bf827865dd6..3cada5a19022 100644 --- a/libs/portable/Cargo.toml +++ b/libs/portable/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rustdesk-portable-packer" -version = "1.3.3" +version = "1.3.4" edition = "2021" description = "RustDesk Remote Desktop" diff --git a/res/PKGBUILD b/res/PKGBUILD index 3da7c98da0ab..d4bef334710c 100644 --- a/res/PKGBUILD +++ b/res/PKGBUILD @@ -1,5 +1,5 @@ pkgname=rustdesk -pkgver=1.3.3 +pkgver=1.3.4 pkgrel=0 epoch= pkgdesc="" diff --git a/res/rpm-flutter-suse.spec b/res/rpm-flutter-suse.spec index 06653a8ce7f0..a18bb2462bfc 100644 --- a/res/rpm-flutter-suse.spec +++ b/res/rpm-flutter-suse.spec @@ -1,5 +1,5 @@ Name: rustdesk -Version: 1.3.3 +Version: 1.3.4 Release: 0 Summary: RPM package License: GPL-3.0 diff --git a/res/rpm-flutter.spec b/res/rpm-flutter.spec index 74115f8877c1..aba6aa21e967 100644 --- a/res/rpm-flutter.spec +++ b/res/rpm-flutter.spec @@ -1,5 +1,5 @@ Name: rustdesk -Version: 1.3.3 +Version: 1.3.4 Release: 0 Summary: RPM package License: GPL-3.0 diff --git a/res/rpm.spec b/res/rpm.spec index 3dfc496c2ec8..5f36a8b56005 100644 --- a/res/rpm.spec +++ b/res/rpm.spec @@ -1,5 +1,5 @@ Name: rustdesk -Version: 1.3.3 +Version: 1.3.4 Release: 0 Summary: RPM package License: GPL-3.0 From 5a2a94d2cc7a2a4894b20af9e741b9c0db974453 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Mon, 2 Dec 2024 00:09:03 +0800 Subject: [PATCH 435/541] fix: macos, input (#10133) 1. Workaround sticky `Fn` for more keys. 2. Workaround stikey `Help`. Signed-off-by: fufesou --- Cargo.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 62f612397b98..7c5843189547 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5219,7 +5219,7 @@ dependencies = [ [[package]] name = "rdev" version = "0.5.0-2" -source = "git+https://github.com/rustdesk-org/rdev#961d25cc00c6b3ef80f444e6a7bed9872e2c35ea" +source = "git+https://github.com/rustdesk-org/rdev#01ac3ec8009f04f7615842b9152338844b806184" dependencies = [ "cocoa 0.24.1", "core-foundation 0.9.4", From dc58c85e30e8df6aec6aa80a16e1a2647e29aee5 Mon Sep 17 00:00:00 2001 From: 21pages Date: Mon, 2 Dec 2024 12:35:44 +0800 Subject: [PATCH 436/541] try fix mac textedit of server config (#10135) Signed-off-by: 21pages --- .../desktop/pages/desktop_setting_page.dart | 69 ++++++++++++------- 1 file changed, 46 insertions(+), 23 deletions(-) diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 0c7586e7036c..577b1e8c5a68 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -1362,11 +1362,30 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin { bool get wantKeepAlive => true; bool locked = !isWeb && bind.mainIsInstalled(); + final scrollController = ScrollController(); + late final TextEditingController idController; + late final TextEditingController relayController; + late final TextEditingController apiController; + late final TextEditingController keyController; + + @override + void initState() { + super.initState(); + Map oldOptions = jsonDecode(bind.mainGetOptionsSync()); + old(String key) { + return (oldOptions[key] ?? '').trim(); + } + + idController = TextEditingController(text: old('custom-rendezvous-server')); + relayController = TextEditingController(text: old('relay-server')); + apiController = TextEditingController(text: old('api-server')); + keyController = TextEditingController(text: old('key')); + } + @override Widget build(BuildContext context) { super.build(context); bool enabled = !locked; - final scrollController = ScrollController(); final hideServer = bind.mainGetBuildinOption(key: kOptionHideServerSetting) == 'Y'; // TODO: support web proxy @@ -1395,19 +1414,10 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin { // Simple temp wrapper for PR check tmpWrapper() { // Setting page is not modal, oldOptions should only be used when getting options, never when setting. - Map oldOptions = jsonDecode(bind.mainGetOptionsSync()); - old(String key) { - return (oldOptions[key] ?? '').trim(); - } RxString idErrMsg = ''.obs; RxString relayErrMsg = ''.obs; RxString apiErrMsg = ''.obs; - var idController = - TextEditingController(text: old('custom-rendezvous-server')); - var relayController = TextEditingController(text: old('relay-server')); - var apiController = TextEditingController(text: old('api-server')); - var keyController = TextEditingController(text: old('key')); final controllers = [ idController, relayController, @@ -2251,26 +2261,39 @@ _LabeledTextField( String errorText, bool enabled, bool secure) { - return Row( + return Table( + columnWidths: const { + 0: FixedColumnWidth(150), + 1: FlexColumnWidth(), + }, + defaultVerticalAlignment: TableCellVerticalAlignment.middle, children: [ - ConstrainedBox( - constraints: const BoxConstraints(minWidth: 140), - child: Text( - '${translate(label)}:', - textAlign: TextAlign.right, - style: TextStyle( - fontSize: 16, color: disabledTextColor(context, enabled)), - ).marginOnly(right: 10)), - Expanded( - child: TextField( + TableRow( + children: [ + Padding( + padding: const EdgeInsets.only(right: 10), + child: Text( + '${translate(label)}:', + textAlign: TextAlign.right, + style: TextStyle( + fontSize: 16, + color: disabledTextColor(context, enabled), + ), + ), + ), + TextField( controller: controller, enabled: enabled, obscureText: secure, + autocorrect: false, decoration: InputDecoration( - errorText: errorText.isNotEmpty ? errorText : null), + errorText: errorText.isNotEmpty ? errorText : null, + ), style: TextStyle( color: disabledTextColor(context, enabled), - )), + ), + ), + ], ), ], ).marginOnly(bottom: 8); From 3251045e22c705f2e20f48449b9378a5d99480ce Mon Sep 17 00:00:00 2001 From: jkh0kr Date: Mon, 2 Dec 2024 16:47:23 +0900 Subject: [PATCH 437/541] Update ko.rs (#10138) Update ko.rs --- src/lang/ko.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 527813d09eba..93c174894119 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -653,7 +653,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", "폴더 업로드"), ("Upload files", "파일 업로드"), ("Clipboard is synchronized", "클립보드가 동기화됨"), - ("Update client clipboard", ""), - ("Untagged", ""), + ("Update client clipboard", "클라이언트 클립보드 업데이트"), + ("Untagged", "태그 없음"), ].iter().cloned().collect(); } From dea99ffb3ab34fee562090d214af419b557debdf Mon Sep 17 00:00:00 2001 From: rustdesk Date: Mon, 2 Dec 2024 16:11:12 +0800 Subject: [PATCH 438/541] fix rustdesk exit crash --- flutter/pubspec.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index 58df59a5521f..ec72da0a4140 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -335,7 +335,7 @@ packages: description: path: "." ref: HEAD - resolved-ref: "4f562ab49d289cfa36bfda7cff12746ec0200033" + resolved-ref: "925df98a740b168ae3dad42592db4af66401053c" url: "https://github.com/rustdesk-org/rustdesk_desktop_multi_window" source: git version: "0.1.0" From 773b9d6645fad2159471881a9ed865e229bbb00b Mon Sep 17 00:00:00 2001 From: 21pages Date: Mon, 2 Dec 2024 17:10:34 +0800 Subject: [PATCH 439/541] win7 uses soft rendering by default (#10139) win7 vm got black screen on remote window with texture rendering Signed-off-by: 21pages --- src/platform/windows.cc | 5 +++++ src/platform/windows.rs | 6 ++++++ src/ui_interface.rs | 39 +++++++++++++++++++++++++++------------ 3 files changed, 38 insertions(+), 12 deletions(-) diff --git a/src/platform/windows.cc b/src/platform/windows.cc index 9ee3c1f5c9a6..04095a2d63f0 100644 --- a/src/platform/windows.cc +++ b/src/platform/windows.cc @@ -222,6 +222,11 @@ extern "C" return IsWindowsServer(); } + bool is_windows_10_or_greater() + { + return IsWindows10OrGreater(); + } + HANDLE LaunchProcessWin(LPCWSTR cmd, DWORD dwSessionId, BOOL as_user, DWORD *pDwTokenPid) { HANDLE hProcess = NULL; diff --git a/src/platform/windows.rs b/src/platform/windows.rs index 4495af538a7b..c0839dc55acd 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -479,6 +479,7 @@ extern "C" { fn selectInputDesktop() -> BOOL; fn inputDesktopSelected() -> BOOL; fn is_windows_server() -> BOOL; + fn is_windows_10_or_greater() -> BOOL; fn handleMask( out: *mut u8, mask: *const u8, @@ -1559,6 +1560,11 @@ pub fn is_win_server() -> bool { unsafe { is_windows_server() > 0 } } +#[inline] +pub fn is_win_10_or_greater() -> bool { + unsafe { is_windows_10_or_greater() > 0 } +} + pub fn bootstrap() { if let Ok(lic) = get_license_from_exe_name() { *config::EXE_RENDEZVOUS_SERVER.write().unwrap() = lic.host.clone(); diff --git a/src/ui_interface.rs b/src/ui_interface.rs index caff46b84f33..5d7f9ee039cd 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -173,21 +173,36 @@ pub fn get_option>(key: T) -> String { } #[inline] -#[cfg(target_os = "macos")] pub fn use_texture_render() -> bool { - cfg!(feature = "flutter") && LocalConfig::get_option(config::keys::OPTION_TEXTURE_RENDER) == "Y" -} + #[cfg(target_os = "android")] + return false; + #[cfg(target_os = "ios")] + return false; -#[inline] -#[cfg(any(target_os = "windows", target_os = "linux"))] -pub fn use_texture_render() -> bool { - cfg!(feature = "flutter") && LocalConfig::get_option(config::keys::OPTION_TEXTURE_RENDER) != "N" -} + #[cfg(target_os = "macos")] + return cfg!(feature = "flutter") + && LocalConfig::get_option(config::keys::OPTION_TEXTURE_RENDER) == "Y"; -#[inline] -#[cfg(any(target_os = "android", target_os = "ios"))] -pub fn use_texture_render() -> bool { - false + #[cfg(target_os = "linux")] + return cfg!(feature = "flutter") + && LocalConfig::get_option(config::keys::OPTION_TEXTURE_RENDER) != "N"; + + #[cfg(target_os = "windows")] + { + if !cfg!(feature = "flutter") { + return false; + } + // https://learn.microsoft.com/en-us/windows/win32/sysinfo/targeting-your-application-at-windows-8-1 + #[cfg(debug_assertions)] + let default_texture = true; + #[cfg(not(debug_assertions))] + let default_texture = crate::platform::is_win_10_or_greater(); + if default_texture { + LocalConfig::get_option(config::keys::OPTION_TEXTURE_RENDER) != "N" + } else { + return LocalConfig::get_option(config::keys::OPTION_TEXTURE_RENDER) == "Y"; + } + } } #[inline] From f38d89aaeefd008689bc98b11c2559576636c669 Mon Sep 17 00:00:00 2001 From: solokot Date: Mon, 2 Dec 2024 13:30:24 +0300 Subject: [PATCH 440/541] Update ru.rs (#10143) --- src/lang/ru.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 582fdf6dcc6a..5979961ddf91 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -654,6 +654,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload files", "Загрузить файлы"), ("Clipboard is synchronized", "Буфер обмена синхронизирован"), ("Update client clipboard", "Обновить буфер обмена клиента"), - ("Untagged", ""), + ("Untagged", "Без метки"), ].iter().cloned().collect(); } From b8d36b6558548602961d4a7ea35373e543608be5 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Mon, 2 Dec 2024 22:29:20 +0800 Subject: [PATCH 441/541] revert multi-window plugin --- flutter/pubspec.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index ec72da0a4140..58df59a5521f 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -335,7 +335,7 @@ packages: description: path: "." ref: HEAD - resolved-ref: "925df98a740b168ae3dad42592db4af66401053c" + resolved-ref: "4f562ab49d289cfa36bfda7cff12746ec0200033" url: "https://github.com/rustdesk-org/rustdesk_desktop_multi_window" source: git version: "0.1.0" From bd0a33e46796a9f24bba6fabb2534490fe1757a0 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Tue, 3 Dec 2024 01:02:41 +0800 Subject: [PATCH 442/541] fix: linux, window, workaround, mint, mate (#10146) * refact: linux, window, workaround, mint, mate Signed-off-by: fufesou * refact: case insensitive Signed-off-by: fufesou --------- Signed-off-by: fufesou --- flutter/lib/common.dart | 30 ++++++++ flutter/lib/main.dart | 11 ++- flutter/lib/web/bridge.dart | 4 ++ flutter/linux/my_application.cc | 98 +++++++++++++++++++++++---- libs/hbb_common/src/platform/linux.rs | 15 +++- src/flutter_ffi.rs | 20 ++++++ 6 files changed, 161 insertions(+), 17 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 4e97449124f7..13ee4dd84a43 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:convert'; import 'dart:math'; +import 'dart:io'; import 'package:back_button_interceptor/back_button_interceptor.dart'; import 'package:desktop_multi_window/desktop_multi_window.dart'; @@ -3459,6 +3460,35 @@ Widget buildPresetPasswordWarning() { ); } +bool get isLinuxMateDesktop => + isLinux && + (Platform.environment['XDG_CURRENT_DESKTOP']?.toLowerCase() == 'mate' || + Platform.environment['XDG_SESSION_DESKTOP']?.toLowerCase() == 'mate' || + Platform.environment['DESKTOP_SESSION']?.toLowerCase() == 'mate'); + +Map? _linuxOsDistro; + +String getLinuxOsDistroId() { + if (_linuxOsDistro == null) { + String osInfo = bind.getOsDistroInfo(); + if (osInfo.isEmpty) { + _linuxOsDistro = {}; + } else { + try { + _linuxOsDistro = jsonDecode(osInfo); + } catch (e) { + debugPrint('Failed to parse os info: $e'); + // Don't call `bind.getOsDistroInfo()` again if failed to parse osInfo. + _linuxOsDistro = {}; + } + } + } + return (_linuxOsDistro?['id'] ?? '') as String; +} + +bool get isLinuxMint => + getLinuxOsDistroId().toLowerCase().contains('linuxmint'); + // https://github.com/leanflutter/window_manager/blob/87dd7a50b4cb47a375b9fc697f05e56eea0a2ab3/lib/src/widgets/virtual_window_frame.dart#L44 Widget buildVirtualWindowFrame(BuildContext context, Widget child) { boxShadow() => isMainDesktopWindow diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index 00afbb001e76..3176bfb86eba 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -483,7 +483,16 @@ class _AppState extends State with WidgetsBindingObserver { child = keyListenerBuilder(context, child); } if (isLinux) { - child = buildVirtualWindowFrame(context, child); + // `(!(isLinuxMateDesktop || isLinuxMint))` is not used here for clarity. + // `isLinuxMint` will call ffi function. + if (!isLinuxMateDesktop) { + if (!isLinuxMint) { + debugPrint( + 'Linux distro is not linuxmint, and desktop is not mate, ' + 'so we build virtual window frame.'); + child = buildVirtualWindowFrame(context, child); + } + } } return child; }, diff --git a/flutter/lib/web/bridge.dart b/flutter/lib/web/bridge.dart index 20891281455d..ffbf6638253d 100644 --- a/flutter/lib/web/bridge.dart +++ b/flutter/lib/web/bridge.dart @@ -1828,5 +1828,9 @@ class RustdeskImpl { throw UnimplementedError("sessionGetConnToken"); } + String getOsDistroInfo({dynamic hint}) { + return ''; + } + void dispose() {} } diff --git a/flutter/linux/my_application.cc b/flutter/linux/my_application.cc index f4247bd94613..9fa947002d3e 100644 --- a/flutter/linux/my_application.cc +++ b/flutter/linux/my_application.cc @@ -17,6 +17,7 @@ G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) extern bool gIsConnectionManager; GtkWidget *find_gl_area(GtkWidget *widget); +void try_set_transparent(GtkWindow* window, GdkScreen* screen, FlView* view); // Implements GApplication::activate. static void my_application_activate(GApplication* application) { @@ -79,21 +80,7 @@ static void my_application_activate(GApplication* application) { gtk_widget_show(GTK_WIDGET(view)); gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); - // https://github.com/flutter/flutter/issues/152154 - // Remove this workaround when flutter version is updated. - GtkWidget *gl_area = find_gl_area(GTK_WIDGET(view)); - if (gl_area != NULL) { - gtk_gl_area_set_has_alpha(GTK_GL_AREA(gl_area), TRUE); - } - - if (screen != NULL) { - GdkVisual *visual = NULL; - gtk_widget_set_app_paintable(GTK_WIDGET(window), TRUE); - visual = gdk_screen_get_rgba_visual(screen); - if (visual != NULL && gdk_screen_is_composited(screen)) { - gtk_widget_set_visual(GTK_WIDGET(window), visual); - } - } + try_set_transparent(window, screen, view); fl_register_plugins(FL_PLUGIN_REGISTRY(view)); @@ -162,3 +149,84 @@ GtkWidget *find_gl_area(GtkWidget *widget) return NULL; } + +bool is_linux_mint() +{ + bool is_mint = false; + char line[256]; + FILE *fp = fopen("/etc/os-release", "r"); + if (fp == NULL) { + return false; + } + while (fgets(line, sizeof(line), fp)) { + if (strstr(line, "ID=linuxmint") != NULL) { + is_mint = true; + break; + } + } + fclose(fp); + + return is_mint; +} + +bool is_desktop_mate() +{ + const char* desktop = NULL; + desktop = getenv("XDG_CURRENT_DESKTOP"); + printf("Linux desktop, XDG_CURRENT_DESKTOP: %s\n", desktop == NULL ? "" : desktop); + if (desktop == NULL) { + desktop = getenv("XDG_SESSION_DESKTOP"); + printf("Linux desktop, XDG_SESSION_DESKTOP: %s\n", desktop == NULL ? "" : desktop); + } + if (desktop == NULL) { + desktop = getenv("DESKTOP_SESSION"); + printf("Linux desktop, DESKTOP_SESSION: %s\n", desktop == NULL ? "" : desktop); + } + if (desktop != NULL && strcasecmp(desktop, "mate") == 0) { + return true; + } + return false; +} + +bool skip_setting_transparent() +{ + if (is_desktop_mate()) { + printf("Linux desktop, MATE\n"); + return true; + } + + if (is_linux_mint()) { + printf("Linux desktop, Linux Mint\n"); + return true; + } + + return false; +} + +// https://github.com/flutter/flutter/issues/152154 +// Remove this workaround when flutter version is updated. +void try_set_transparent(GtkWindow* window, GdkScreen* screen, FlView* view) +{ + GtkWidget *gl_area = NULL; + + if (skip_setting_transparent()) { + printf("Skip setting transparent\n"); + return; + } + + printf("Try setting transparent\n"); + + gl_area = find_gl_area(GTK_WIDGET(view)); + if (gl_area != NULL) { + gtk_gl_area_set_has_alpha(GTK_GL_AREA(gl_area), TRUE); + } + + if (screen != NULL) { + GdkVisual *visual = NULL; + gtk_widget_set_app_paintable(GTK_WIDGET(window), TRUE); + visual = gdk_screen_get_rgba_visual(screen); + if (visual != NULL && gdk_screen_is_composited(screen)) { + gtk_widget_set_visual(GTK_WIDGET(window), visual); + } + } +} diff --git a/libs/hbb_common/src/platform/linux.rs b/libs/hbb_common/src/platform/linux.rs index 60c8714d8212..31481ca78f23 100644 --- a/libs/hbb_common/src/platform/linux.rs +++ b/libs/hbb_common/src/platform/linux.rs @@ -13,22 +13,35 @@ pub const XDG_CURRENT_DESKTOP: &str = "XDG_CURRENT_DESKTOP"; pub struct Distro { pub name: String, + pub id: String, pub version_id: String, } impl Distro { fn new() -> Self { + // to-do: + // 1. Remove `run_cmds`, read file once + // 2. Add more distro infos let name = run_cmds("awk -F'=' '/^NAME=/ {print $2}' /etc/os-release") .unwrap_or_default() .trim() .trim_matches('"') .to_string(); + let id = run_cmds("awk -F'=' '/^ID=/ {print $2}' /etc/os-release") + .unwrap_or_default() + .trim() + .trim_matches('"') + .to_string(); let version_id = run_cmds("awk -F'=' '/^VERSION_ID=/ {print $2}' /etc/os-release") .unwrap_or_default() .trim() .trim_matches('"') .to_string(); - Self { name, version_id } + Self { + name, + id, + version_id, + } } } diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 4c875be49b72..4630ac3337d7 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -19,6 +19,7 @@ use hbb_common::allow_err; use hbb_common::{ config::{self, LocalConfig, PeerConfig, PeerInfoSerde}, fs, lazy_static, log, + message_proto::Hash, rendezvous_proto::ConnType, ResultType, }; @@ -2341,6 +2342,25 @@ pub fn main_audio_support_loopback() -> SyncReturn { SyncReturn(is_surpport) } +pub fn get_os_distro_info() -> SyncReturn { + #[cfg(target_os = "linux")] + { + let distro = &hbb_common::platform::linux::DISTRO; + SyncReturn( + serde_json::to_string(&HashMap::from([ + ("name", distro.name.clone()), + ("id", distro.id.clone()), + ("version_id", distro.version_id.clone()), + ])) + .unwrap_or_default(), + ) + } + #[cfg(not(target_os = "linux"))] + { + SyncReturn("".to_owned()) + } +} + #[cfg(target_os = "android")] pub mod server_side { use hbb_common::{config, log}; From 34d2c62781d0472df8bc17e9e9aa569be9b22419 Mon Sep 17 00:00:00 2001 From: 21pages Date: Tue, 3 Dec 2024 14:14:29 +0800 Subject: [PATCH 443/541] set id/relay server with a dialog (#10150) Signed-off-by: 21pages --- .../desktop/pages/desktop_setting_page.dart | 146 +++++++----------- flutter/lib/mobile/pages/settings_page.dart | 5 - flutter/lib/mobile/widgets/dialog.dart | 131 ++++++++++------ 3 files changed, 140 insertions(+), 142 deletions(-) diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 577b1e8c5a68..6f220a35f7b2 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -11,6 +11,7 @@ import 'package:flutter_hbb/common/widgets/setting_widgets.dart'; import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/desktop/pages/desktop_home_page.dart'; import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart'; +import 'package:flutter_hbb/mobile/widgets/dialog.dart'; import 'package:flutter_hbb/models/platform_model.dart'; import 'package:flutter_hbb/models/server_model.dart'; import 'package:flutter_hbb/plugin/manager.dart'; @@ -1363,34 +1364,10 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin { bool locked = !isWeb && bind.mainIsInstalled(); final scrollController = ScrollController(); - late final TextEditingController idController; - late final TextEditingController relayController; - late final TextEditingController apiController; - late final TextEditingController keyController; - - @override - void initState() { - super.initState(); - Map oldOptions = jsonDecode(bind.mainGetOptionsSync()); - old(String key) { - return (oldOptions[key] ?? '').trim(); - } - - idController = TextEditingController(text: old('custom-rendezvous-server')); - relayController = TextEditingController(text: old('relay-server')); - apiController = TextEditingController(text: old('api-server')); - keyController = TextEditingController(text: old('key')); - } @override Widget build(BuildContext context) { super.build(context); - bool enabled = !locked; - final hideServer = - bind.mainGetBuildinOption(key: kOptionHideServerSetting) == 'Y'; - // TODO: support web proxy - final hideProxy = - isWeb || bind.mainGetBuildinOption(key: kOptionHideProxySetting) == 'Y'; return ListView(controller: scrollController, children: [ _lock(locked, 'Unlock Network Settings', () { locked = false; @@ -1399,80 +1376,69 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin { AbsorbPointer( absorbing: locked, child: Column(children: [ - if (!hideServer) server(enabled), - if (!hideProxy) - _Card(title: 'Proxy', children: [ - _Button('Socks5/Http(s) Proxy', changeSocks5Proxy, - enabled: enabled), - ]), + network(context), ]), ), ]).marginOnly(bottom: _kListViewBottomMargin); } - server(bool enabled) { - // Simple temp wrapper for PR check - tmpWrapper() { - // Setting page is not modal, oldOptions should only be used when getting options, never when setting. - - RxString idErrMsg = ''.obs; - RxString relayErrMsg = ''.obs; - RxString apiErrMsg = ''.obs; - final controllers = [ - idController, - relayController, - apiController, - keyController, - ]; - final errMsgs = [ - idErrMsg, - relayErrMsg, - apiErrMsg, - ]; - - submit() async { - bool result = await setServerConfig( - null, - errMsgs, - ServerConfig( - idServer: idController.text, - relayServer: relayController.text, - apiServer: apiController.text, - key: keyController.text)); - if (result) { - setState(() {}); - showToast(translate('Successful')); - } else { - showToast(translate('Failed')); - } - } + Widget network(BuildContext context) { + final hideServer = + bind.mainGetBuildinOption(key: kOptionHideServerSetting) == 'Y'; + final hideProxy = + isWeb || bind.mainGetBuildinOption(key: kOptionHideProxySetting) == 'Y'; - bool secure = !enabled; - return _Card( - title: 'ID/Relay Server', - title_suffix: ServerConfigImportExportWidgets(controllers, errMsgs), - children: [ - Column( - children: [ - Obx(() => _LabeledTextField(context, 'ID Server', idController, - idErrMsg.value, enabled, secure)), - if (!isWeb) - Obx(() => _LabeledTextField(context, 'Relay Server', - relayController, relayErrMsg.value, enabled, secure)), - Obx(() => _LabeledTextField(context, 'API Server', - apiController, apiErrMsg.value, enabled, secure)), - _LabeledTextField( - context, 'Key', keyController, '', enabled, secure), - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [_Button('Apply', submit, enabled: enabled)], - ).marginOnly(top: 10), - ], - ) - ]); + if (hideServer && hideProxy) { + return Offstage(); } - return tmpWrapper(); + return _Card( + title: 'Network', + children: [ + Container( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (!hideServer) + ListTile( + leading: Icon(Icons.dns_outlined, color: _accentColor), + title: Text( + translate('ID/Relay Server'), + style: TextStyle(fontSize: _kContentFontSize), + ), + enabled: !locked, + onTap: () => showServerSettings(gFFI.dialogManager), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + contentPadding: EdgeInsets.symmetric(horizontal: 16), + minLeadingWidth: 0, + horizontalTitleGap: 10, + ), + if (!hideServer && !hideProxy) + Divider(height: 1, indent: 16, endIndent: 16), + if (!hideProxy) + ListTile( + leading: + Icon(Icons.network_ping_outlined, color: _accentColor), + title: Text( + translate('Socks5/Http(s) Proxy'), + style: TextStyle(fontSize: _kContentFontSize), + ), + enabled: !locked, + onTap: changeSocks5Proxy, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + contentPadding: EdgeInsets.symmetric(horizontal: 16), + minLeadingWidth: 0, + horizontalTitleGap: 10, + ), + ], + ), + ), + ], + ); } } diff --git a/flutter/lib/mobile/pages/settings_page.dart b/flutter/lib/mobile/pages/settings_page.dart index eb3865933143..9e265810fc98 100644 --- a/flutter/lib/mobile/pages/settings_page.dart +++ b/flutter/lib/mobile/pages/settings_page.dart @@ -828,11 +828,6 @@ class _SettingsState extends State with WidgetsBindingObserver { } } -void showServerSettings(OverlayDialogManager dialogManager) async { - Map options = jsonDecode(await bind.mainGetOptions()); - showServerSettingsWithValue(ServerConfig.fromOptions(options), dialogManager); -} - void showLanguageSettings(OverlayDialogManager dialogManager) async { try { final langs = json.decode(await bind.mainGetLangs()) as List; diff --git a/flutter/lib/mobile/widgets/dialog.dart b/flutter/lib/mobile/widgets/dialog.dart index 2d17f3b5438c..1c8b4dd3d7b8 100644 --- a/flutter/lib/mobile/widgets/dialog.dart +++ b/flutter/lib/mobile/widgets/dialog.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/common/widgets/setting_widgets.dart'; import 'package:flutter_hbb/common/widgets/toolbar.dart'; @@ -146,6 +147,16 @@ void setTemporaryPasswordLengthDialog( }, backDismiss: true, clickMaskDismiss: true); } +void showServerSettings(OverlayDialogManager dialogManager) async { + Map options = {}; + try { + options = jsonDecode(await bind.mainGetOptions()); + } catch (e) { + print("Invalid server config: $e"); + } + showServerSettingsWithValue(ServerConfig.fromOptions(options), dialogManager); +} + void showServerSettingsWithValue( ServerConfig serverConfig, OverlayDialogManager dialogManager) async { var isInProgress = false; @@ -184,6 +195,43 @@ void showServerSettingsWithValue( return ret; } + Widget buildField( + String label, TextEditingController controller, String errorMsg, + {String? Function(String?)? validator, bool autofocus = false}) { + if (isDesktop || isWeb) { + return Row( + children: [ + SizedBox( + width: 120, + child: Text(label), + ), + SizedBox(width: 8), + Expanded( + child: TextFormField( + controller: controller, + decoration: InputDecoration( + errorText: errorMsg.isEmpty ? null : errorMsg, + contentPadding: + EdgeInsets.symmetric(horizontal: 8, vertical: 12), + ), + validator: validator, + autofocus: autofocus, + ), + ), + ], + ); + } + + return TextFormField( + controller: controller, + decoration: InputDecoration( + labelText: label, + errorText: errorMsg.isEmpty ? null : errorMsg, + ), + validator: validator, + ); + } + return CustomAlertDialog( title: Row( children: [ @@ -191,56 +239,45 @@ void showServerSettingsWithValue( ...ServerConfigImportExportWidgets(controllers, errMsgs), ], ), - content: Form( + content: ConstrainedBox( + constraints: const BoxConstraints(minWidth: 500), + child: Form( child: Obx(() => Column( - mainAxisSize: MainAxisSize.min, - children: [ - TextFormField( - controller: idCtrl, - decoration: InputDecoration( - labelText: translate('ID Server'), - errorText: idServerMsg.value.isEmpty - ? null - : idServerMsg.value), - ) - ] + - [ - if (isAndroid) - TextFormField( - controller: relayCtrl, - decoration: InputDecoration( - labelText: translate('Relay Server'), - errorText: relayServerMsg.value.isEmpty - ? null - : relayServerMsg.value), - ) - ] + - [ - TextFormField( - controller: apiCtrl, - decoration: InputDecoration( - labelText: translate('API Server'), - ), - autovalidateMode: AutovalidateMode.onUserInteraction, - validator: (v) { - if (v != null && v.isNotEmpty) { - if (!(v.startsWith('http://') || - v.startsWith("https://"))) { - return translate("invalid_http"); - } + mainAxisSize: MainAxisSize.min, + children: [ + buildField(translate('ID Server'), idCtrl, idServerMsg.value, + autofocus: true), + SizedBox(height: 8), + if (!isIOS && !isWeb) ...[ + buildField(translate('Relay Server'), relayCtrl, + relayServerMsg.value), + SizedBox(height: 8), + ], + buildField( + translate('API Server'), + apiCtrl, + apiServerMsg.value, + validator: (v) { + if (v != null && v.isNotEmpty) { + if (!(v.startsWith('http://') || + v.startsWith("https://"))) { + return translate("invalid_http"); } - return null; - }, - ), - TextFormField( - controller: keyCtrl, - decoration: InputDecoration( - labelText: 'Key', - ), + } + return null; + }, + ), + SizedBox(height: 8), + buildField('Key', keyCtrl, ''), + if (isInProgress) + Padding( + padding: EdgeInsets.only(top: 8), + child: LinearProgressIndicator(), ), - // NOT use Offstage to wrap LinearProgressIndicator - if (isInProgress) const LinearProgressIndicator(), - ]))), + ], + )), + ), + ), actions: [ dialogButton('Cancel', onPressed: () { close(); From e6edf39305d82d802a06cd718fed39e0fb70c700 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Tue, 3 Dec 2024 14:22:20 +0800 Subject: [PATCH 444/541] fix: support emptry folder transfer for web (#10151) Signed-off-by: fufesou --- flutter/lib/models/file_model.dart | 32 ++++++++++++++++++++++++++++-- flutter/lib/models/model.dart | 4 ++++ flutter/lib/web/bridge.dart | 20 +++++++++++++++++++ 3 files changed, 54 insertions(+), 2 deletions(-) diff --git a/flutter/lib/models/file_model.dart b/flutter/lib/models/file_model.dart index d4ace7578e24..4a00b803e340 100644 --- a/flutter/lib/models/file_model.dart +++ b/flutter/lib/models/file_model.dart @@ -261,6 +261,27 @@ class FileModel { debugPrint("Failed to decode onSelectedFiles: $e"); } } + + void sendEmptyDirs(dynamic obj) { + late final List emptyDirs; + try { + emptyDirs = jsonDecode(obj['dirs'] as String); + } catch (e) { + debugPrint("Failed to decode sendEmptyDirs: $e"); + } + final otherSideData = remoteController.directoryData(); + final toPath = otherSideData.directory.path; + final isPeerWindows = otherSideData.options.isWindows; + + final isLocalWindows = isWindows || isWebOnWindows; + for (var dir in emptyDirs) { + if (isLocalWindows != isPeerWindows) { + dir = PathUtil.convert(dir, isLocalWindows, isPeerWindows); + } + var peerPath = PathUtil.join(toPath, dir, isPeerWindows); + remoteController.createDirWithRemote(peerPath, true); + } + } } class DirectoryData { @@ -502,8 +523,9 @@ class FileController { "path: ${from.path}, toPath: $toPath, to: ${PathUtil.join(toPath, from.name, isWindows)}"); } - if (!isLocal && - versionCmp(rootState.target!.ffiModel.pi.version, '1.3.3') < 0) { + if (isWeb || + (!isLocal && + versionCmp(rootState.target!.ffiModel.pi.version, '1.3.3') < 0)) { return; } @@ -1506,6 +1528,12 @@ class PathUtil { return pathUtil.split(path); } + static String convert(String path, bool isMainWindows, bool isOtherWindows) { + final mainPathUtil = isMainWindows ? windowsContext : posixContext; + final otherPathUtil = isOtherWindows ? windowsContext : posixContext; + return otherPathUtil.joinAll(mainPathUtil.split(path)); + } + static String dirname(String path, bool isWindows) { final pathUtil = isWindows ? windowsContext : posixContext; return pathUtil.dirname(path); diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index e5cde939fb76..5a5dcf623eed 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -402,6 +402,10 @@ class FfiModel with ChangeNotifier { if (isWeb) { parent.target?.fileModel.onSelectedFiles(evt); } + } else if (name == "send_emptry_dirs") { + if (isWeb) { + parent.target?.fileModel.sendEmptyDirs(evt); + } } else if (name == "record_status") { if (desktopType == DesktopType.remote || isMobile) { parent.target?.recordingModel.updateStatus(evt['start'] == 'true'); diff --git a/flutter/lib/web/bridge.dart b/flutter/lib/web/bridge.dart index ffbf6638253d..498a464aee3e 100644 --- a/flutter/lib/web/bridge.dart +++ b/flutter/lib/web/bridge.dart @@ -1801,6 +1801,26 @@ class RustdeskImpl { throw UnimplementedError("mainMaxEncryptLen"); } + bool mainAudioSupportLoopback({dynamic hint}) { + return false; + } + + Future sessionReadLocalEmptyDirsRecursiveSync( + {required UuidValue sessionId, + required String path, + required bool includeHidden, + dynamic hint}) { + throw UnimplementedError("sessionReadLocalEmptyDirsRecursiveSync"); + } + + Future sessionReadRemoteEmptyDirsRecursiveSync( + {required UuidValue sessionId, + required String path, + required bool includeHidden, + dynamic hint}) { + throw UnimplementedError("sessionReadRemoteEmptyDirsRecursiveSync"); + } + Future sessionRenameFile( {required UuidValue sessionId, required int actId, From fd67be4a16067b9add78dfbeb3f5186c89a062fb Mon Sep 17 00:00:00 2001 From: Yevhen Popok Date: Wed, 4 Dec 2024 04:31:13 +0200 Subject: [PATCH 445/541] Update uk.rs (#10162) --- src/lang/uk.rs | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/src/lang/uk.rs b/src/lang/uk.rs index 6b5ec4381887..1a8a981e3665 100644 --- a/src/lang/uk.rs +++ b/src/lang/uk.rs @@ -120,9 +120,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Original", "Оригінал"), ("Shrink", "Зменшити"), ("Stretch", "Розтягнути"), - ("Scrollbar", "Смуга прокрутки"), - ("ScrollAuto", "Автоматична прокрутка"), - ("Good image quality", "Хороша якість зображення"), + ("Scrollbar", "Смужка гортання"), + ("ScrollAuto", "Автоматичне гортання"), + ("Good image quality", "Гарна якість зображення"), ("Balanced", "Збалансована"), ("Optimize reaction time", "Оптимізувати час реакції"), ("Custom", "Користувацька"), @@ -199,10 +199,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please enter the folder name", "Будь ласка, введіть назву для теки"), ("Fix it", "Виправити"), ("Warning", "Попередження"), - ("Login screen using Wayland is not supported", "Вхід в систему з використанням Wayland не підтримується"), + ("Login screen using Wayland is not supported", "Екран входу, який використовує Wayland, не підтримується"), ("Reboot required", "Потрібне перезавантаження"), ("Unsupported display server", "Графічний сервер не підтримується"), - ("x11 expected", "Очікується X11"), + ("x11 expected", "Потрібен X11"), ("Port", "Порт"), ("Settings", "Налаштування"), ("Username", "Імʼя користувача"), @@ -220,21 +220,21 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Verification code", "Код підтвердження"), ("verification_tip", "Код підтвердження надіслано на зареєстровану email-адресу, введіть код підтвердження для продовження авторизації."), ("Logout", "Вийти"), - ("Tags", "Теги"), + ("Tags", "Мітки"), ("Search ID", "Пошук за ID"), ("whitelist_sep", "Відокремлення комою, крапкою з комою, пропуском або новим рядком"), ("Add ID", "Додати ID"), - ("Add Tag", "Додати ключове слово"), - ("Unselect all tags", "Скасувати вибір усіх тегів"), + ("Add Tag", "Додати мітку"), + ("Unselect all tags", "Скасувати вибір усіх міток"), ("Network error", "Помилка мережі"), ("Username missed", "Імʼя користувача відсутнє"), ("Password missed", "Пароль відсутній"), ("Wrong credentials", "Неправильні дані"), ("The verification code is incorrect or has expired", "Код підтвердження некоректний або протермінований"), - ("Edit Tag", "Редагувати тег"), + ("Edit Tag", "Редагувати мітку"), ("Forget Password", "Не зберігати пароль"), ("Favorites", "Вибране"), - ("Add to Favorites", "Додати в обране"), + ("Add to Favorites", "Додати до обраного"), ("Remove from Favorites", "Видалити з обраного"), ("Empty", "Пусто"), ("Invalid folder name", "Неприпустима назва теки"), @@ -279,7 +279,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Screen Connection", "Підключення екрана"), ("Do you accept?", "Ви згодні?"), ("Open System Setting", "Відкрити налаштування системи"), - ("How to get Android input permission?", "Як отримати дозвіл на введення Android?"), + ("How to get Android input permission?", "Як отримати дозвіл на введення в Android?"), ("android_input_permission_tip1", "Для того, щоб віддалений пристрій міг керувати вашим Android-пристроєм за допомогою миші або дотику, вам необхідно дозволити RustDesk використовувати службу \"Спеціальні можливості\"."), ("android_input_permission_tip2", "Будь ласка, перейдіть на наступну сторінку системних налаштувань, знайдіть та увійдіть у [Встановлені служби], увімкніть службу [RustDesk Input]."), ("android_new_connection_tip", "Отримано новий запит на керування вашим поточним пристроєм."), @@ -329,15 +329,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Settings", "Налаштування дисплею"), ("Ratio", "Співвідношення"), ("Image Quality", "Якість зображення"), - ("Scroll Style", "Стиль прокрутки"), + ("Scroll Style", "Стиль гортання"), ("Show Toolbar", "Показати панель інструментів"), ("Hide Toolbar", "Приховати панель інструментів"), ("Direct Connection", "Пряме підключення"), ("Relay Connection", "Ретрансльоване підключення"), ("Secure Connection", "Безпечне підключення"), ("Insecure Connection", "Небезпечне підключення"), - ("Scale original", "Оригінал масштабу"), - ("Scale adaptive", "Масштаб адаптивний"), + ("Scale original", "Оригінальний масштаб"), + ("Scale adaptive", "Адаптивний масштаб"), ("General", "Загальні"), ("Security", "Безпека"), ("Theme", "Тема"), @@ -402,7 +402,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Hide connection management window", "Приховати вікно керування підключеннями"), ("hide_cm_tip", "Дозволено приховати лише якщо сеанс підтверджується постійним паролем"), ("wayland_experiment_tip", "Підтримка Wayland на експериментальній стадії, будь ласка, використовуйте X11, якщо необхідний автоматичний доступ."), - ("Right click to select tabs", "Правий клік для вибору вкладки"), + ("Right click to select tabs", "Вибір вкладок клацанням правою"), ("Skipped", "Пропущено"), ("Add to address book", "Додати IP до Адресної книги"), ("Group", "Група"), @@ -513,7 +513,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop", "Зупинити"), ("exceed_max_devices", "У вас максимальна кількість керованих пристроїв."), ("Sync with recent sessions", "Синхронізація з нещодавніми сеансами"), - ("Sort tags", "Сортувати теги"), + ("Sort tags", "Сортувати мітки"), ("Open connection in new tab", "Відкрити підключення в новій вкладці"), ("Move tab to new window", "Перемістити вкладку до нового вікна"), ("Can not be empty", "Не може бути порожнім"), @@ -524,7 +524,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Grid View", "Перегляд ґраткою"), ("List View", "Перегляд списком"), ("Select", "Вибрати"), - ("Toggle Tags", "Видимість тегів"), + ("Toggle Tags", "Видимість міток"), ("pull_ab_failed_tip", "Не вдалося оновити адресну книгу"), ("push_ab_failed_tip", "Не вдалося синхронізувати адресну книгу"), ("synced_peer_readded_tip", "Пристрої з нещодавніх сеансів будуть синхронізовані з адресною книгою."), @@ -533,7 +533,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("HSV Color", "Колір HSV"), ("Installation Successful!", "Успішне встановлення!"), ("Installation failed!", "Невдале встановлення!"), - ("Reverse mouse wheel", "Зворотній напрям прокрутки"), + ("Reverse mouse wheel", "Зворотній напрям гортання"), ("{} sessions", "{} сеансів"), ("scam_title", "Вас можуть ОБМАНУТИ!"), ("scam_text1", "Якщо ви розмовляєте по телефону з кимось, кого НЕ ЗНАЄТЕ чи кому НЕ ДОВІРЯЄТЕ, і ця особа хоче, щоб ви використали RustDesk та запустили службу, не робіть цього та негайно завершіть дзвінок."), @@ -654,6 +654,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload files", "Надіслати файли"), ("Clipboard is synchronized", "Буфер обміну синхронізовано"), ("Update client clipboard", "Оновити буфер обміну клієнта"), - ("Untagged", ""), + ("Untagged", "Без міток"), ].iter().cloned().collect(); } From 3d17bf4990b31701a58b1fd6a3f1af3c7260a949 Mon Sep 17 00:00:00 2001 From: 21pages Date: Wed, 4 Dec 2024 17:10:10 +0800 Subject: [PATCH 446/541] linux dynamic load libva (#10171) 1. Linux dynamic load libva, which can fix lack of libva dependency for appimage or flatpak, also fix libva version mismatch between build and run. 2. Remove libvdpau, it's not used, and add libva2 explicitly for deb and appimage 3. Print FFmpeg configure log to know the actual codecs. Test * ubuntu 22.04 x64 - [x] deb - [x] flatpak - [x] appimage * ubuntu 18.04 * deb: fcntl64 not found - [x]:appimage - [ ]: platpak hwcodec example: - [x]: combination of lacking any of libva2, libva-x11-2, libva-drm2, intel-media-va-driver - [ ] federa - [ ] arch - [ ] arm64: my ci can't finish arm64 building Signed-off-by: 21pages --- .github/workflows/flutter-build.yml | 14 +- Cargo.lock | 6 +- appimage/AppImageBuilder-aarch64.yml | 2 +- appimage/AppImageBuilder-x86_64.yml | 2 +- build.py | 4 +- flutter/build_android_deps.sh | 1 + res/PKGBUILD | 2 +- res/rpm-flutter-suse.spec | 2 +- res/rpm-flutter.spec | 2 +- res/rpm-suse.spec | 2 +- res/rpm.spec | 2 +- .../0005-mediacodec-changing-bitrate.patch | 38 +- .../ffmpeg/patch/0006-dlopen-libva.patch | 1993 +++++++++++++++++ res/vcpkg/ffmpeg/portfile.cmake | 17 +- 14 files changed, 2038 insertions(+), 49 deletions(-) create mode 100644 res/vcpkg/ffmpeg/patch/0006-dlopen-libva.patch diff --git a/.github/workflows/flutter-build.yml b/.github/workflows/flutter-build.yml index 5ac769ea65df..200fb92f7123 100644 --- a/.github/workflows/flutter-build.yml +++ b/.github/workflows/flutter-build.yml @@ -156,6 +156,7 @@ jobs: done exit 1 fi + head -n 100 "${VCPKG_ROOT}/buildtrees/ffmpeg/build-${{ matrix.job.vcpkg-triplet }}-rel-out.log" || true shell: bash - name: Build rustdesk @@ -316,6 +317,7 @@ jobs: done exit 1 fi + head -n 100 "${VCPKG_ROOT}/buildtrees/ffmpeg/build-${{ matrix.job.vcpkg-triplet }}-rel-out.log" || true shell: bash - name: Build rustdesk @@ -519,6 +521,7 @@ jobs: done exit 1 fi + head -n 100 "${VCPKG_ROOT}/buildtrees/ffmpeg/build-${{ matrix.job.vcpkg-triplet }}-rel-out.log" || true shell: bash - name: Install Rust toolchain @@ -637,6 +640,7 @@ jobs: 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, + vcpkg-triplet: x64-osx, } - { target: aarch64-apple-darwin, @@ -644,6 +648,7 @@ jobs: # extra-build-args: "--disable-flutter-texture-render", # disable this for mac, because we see a lot of users reporting flickering both on arm and x64, and we can not confirm if texture rendering has better performance if htere is no vram, https://github.com/rustdesk/rustdesk/issues/6296 extra-build-args: "--screencapturekit", arch: aarch64, + vcpkg-triplet: arm64-osx, } steps: - name: Export GitHub Actions cache environment variables @@ -755,6 +760,7 @@ jobs: done exit 1 fi + head -n 100 "${VCPKG_ROOT}/buildtrees/ffmpeg/build-${{ matrix.job.vcpkg-triplet }}-rel-out.log" || true - name: Show version information (Rust, cargo, Clang) shell: bash @@ -931,7 +937,6 @@ jobs: libpam0g-dev \ libpulse-dev \ libva-dev \ - libvdpau-dev \ libxcb-randr0-dev \ libxcb-shape0-dev \ libxcb-xfixes0-dev \ @@ -1208,7 +1213,6 @@ jobs: libpam0g-dev \ libpulse-dev \ libva-dev \ - libvdpau-dev \ libxcb-randr0-dev \ libxcb-shape0-dev \ libxcb-xfixes0-dev \ @@ -1440,7 +1444,7 @@ jobs: - name: Install vcpkg dependencies if: matrix.job.arch == 'x86_64' || env.UPLOAD_ARTIFACT == 'true' run: | - sudo apt install -y libva-dev libvdpau-dev + sudo apt install -y libva-dev && apt show libva-dev if ! $VCPKG_ROOT/vcpkg \ install \ --triplet ${{ matrix.job.vcpkg-triplet }} \ @@ -1454,6 +1458,7 @@ jobs: done exit 1 fi + head -n 100 "${VCPKG_ROOT}/buildtrees/ffmpeg/build-${{ matrix.job.vcpkg-triplet }}-rel-out.log" || true shell: bash - name: Restore bridge files @@ -1498,7 +1503,6 @@ jobs: libpam0g-dev \ libpulse-dev \ libva-dev \ - libvdpau-dev \ libxcb-randr0-dev \ libxcb-shape0-dev \ libxcb-xfixes0-dev \ @@ -1771,7 +1775,6 @@ jobs: libpam0g-dev \ libpulse-dev \ libva-dev \ - libvdpau-dev \ libxcb-randr0-dev \ libxcb-shape0-dev \ libxcb-xfixes0-dev \ @@ -1859,6 +1862,7 @@ jobs: done exit 1 fi + head -n 100 "${VCPKG_ROOT}/buildtrees/ffmpeg/build-${{ matrix.job.vcpkg-triplet }}-rel-out.log" || true # build rustdesk python3 ./res/inline-sciter.py export CARGO_INCREMENTAL=0 diff --git a/Cargo.lock b/Cargo.lock index 7c5843189547..7d0b26d71e01 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3064,8 +3064,8 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hwcodec" -version = "0.7.0" -source = "git+https://github.com/rustdesk-org/hwcodec#da7dab48df19edb5a7138ff9e01bf9f148b523da" +version = "0.7.1" +source = "git+https://github.com/rustdesk-org/hwcodec#835e599ed229e4e01b6fa3566e02ea45c73e2e9c" dependencies = [ "bindgen 0.59.2", "cc", @@ -3519,7 +3519,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e310b3a6b5907f99202fcdb4960ff45b93735d7c7d96b760fcff8db2dc0e103d" dependencies = [ "cfg-if 1.0.0", - "windows-targets 0.48.5", + "windows-targets 0.52.5", ] [[package]] diff --git a/appimage/AppImageBuilder-aarch64.yml b/appimage/AppImageBuilder-aarch64.yml index 65a73ee52b68..21ddd3f3d886 100644 --- a/appimage/AppImageBuilder-aarch64.yml +++ b/appimage/AppImageBuilder-aarch64.yml @@ -47,9 +47,9 @@ AppDir: - libasound2 - libsystemd0 - curl + - libva2 - libva-drm2 - libva-x11-2 - - libvdpau1 - libgstreamer-plugins-base1.0-0 - gstreamer1.0-pipewire - libwayland-client0 diff --git a/appimage/AppImageBuilder-x86_64.yml b/appimage/AppImageBuilder-x86_64.yml index 430400721a8b..0f4b6b7e32f9 100644 --- a/appimage/AppImageBuilder-x86_64.yml +++ b/appimage/AppImageBuilder-x86_64.yml @@ -50,9 +50,9 @@ AppDir: - libasound2 - libsystemd0 - curl + - libva2 - libva-drm2 - libva-x11-2 - - libvdpau1 - libgstreamer-plugins-base1.0-0 - gstreamer1.0-pipewire - libwayland-client0 diff --git a/build.py b/build.py index 174bd18ebdf0..5d9740920375 100755 --- a/build.py +++ b/build.py @@ -111,7 +111,7 @@ def make_parser(): '--hwcodec', action='store_true', help='Enable feature hwcodec' + ( - '' if windows or osx else ', need libva-dev, libvdpau-dev.') + '' if windows or osx else ', need libva-dev.') ) parser.add_argument( '--vram', @@ -298,7 +298,7 @@ def generate_control_file(version): Architecture: %s Maintainer: rustdesk 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, gstreamer1.0-pipewire%s +Depends: libgtk-3-0, libxcb-randr0, libxdo3, libxfixes3, libxcb-shape0, libxcb-xfixes0, libasound2, libsystemd0, curl, libva2, libva-drm2, libva-x11-2, libgstreamer-plugins-base1.0-0, libpam0g, gstreamer1.0-pipewire%s Recommends: libayatana-appindicator3-1 Description: A remote control software. diff --git a/flutter/build_android_deps.sh b/flutter/build_android_deps.sh index e4210477e360..64fb9dad2a90 100755 --- a/flutter/build_android_deps.sh +++ b/flutter/build_android_deps.sh @@ -68,6 +68,7 @@ function build { pushd "$SCRIPTDIR/.." $VCPKG_ROOT/vcpkg install --triplet $VCPKG_TARGET --x-install-root="$VCPKG_ROOT/installed" popd + head -n 100 "${VCPKG_ROOT}/buildtrees/ffmpeg/build-$VCPKG_TARGET-rel-out.log" || true echo "*** [$ANDROID_ABI][Finished] Build and install vcpkg dependencies" if [ -d "$VCPKG_ROOT/installed/arm-neon-android" ]; then diff --git a/res/PKGBUILD b/res/PKGBUILD index d4bef334710c..c9a4eb19656d 100644 --- a/res/PKGBUILD +++ b/res/PKGBUILD @@ -7,7 +7,7 @@ arch=('x86_64') url="" license=('AGPL-3.0') groups=() -depends=('gtk3' 'xdotool' 'libxcb' 'libxfixes' 'alsa-lib' 'libva' 'libvdpau' 'libappindicator-gtk3' 'pam' 'gst-plugins-base' 'gst-plugin-pipewire') +depends=('gtk3' 'xdotool' 'libxcb' 'libxfixes' 'alsa-lib' 'libva' 'libappindicator-gtk3' 'pam' 'gst-plugins-base' 'gst-plugin-pipewire') makedepends=() checkdepends=() optdepends=() diff --git a/res/rpm-flutter-suse.spec b/res/rpm-flutter-suse.spec index a18bb2462bfc..433d37973b83 100644 --- a/res/rpm-flutter-suse.spec +++ b/res/rpm-flutter-suse.spec @@ -5,7 +5,7 @@ Summary: RPM package License: GPL-3.0 URL: https://rustdesk.com Vendor: rustdesk -Requires: gtk3 libxcb1 xdotool libXfixes3 alsa-utils libXtst6 libvdpau1 libva2 pam gstreamer-plugins-base gstreamer-plugin-pipewire +Requires: gtk3 libxcb1 xdotool libXfixes3 alsa-utils libXtst6 libva2 pam gstreamer-plugins-base gstreamer-plugin-pipewire Recommends: libayatana-appindicator3-1 Provides: libdesktop_drop_plugin.so()(64bit), libdesktop_multi_window_plugin.so()(64bit), libfile_selector_linux_plugin.so()(64bit), libflutter_custom_cursor_plugin.so()(64bit), libflutter_linux_gtk.so()(64bit), libscreen_retriever_plugin.so()(64bit), libtray_manager_plugin.so()(64bit), liburl_launcher_linux_plugin.so()(64bit), libwindow_manager_plugin.so()(64bit), libwindow_size_plugin.so()(64bit), libtexture_rgba_renderer_plugin.so()(64bit) diff --git a/res/rpm-flutter.spec b/res/rpm-flutter.spec index aba6aa21e967..8d7de5637b1b 100644 --- a/res/rpm-flutter.spec +++ b/res/rpm-flutter.spec @@ -5,7 +5,7 @@ Summary: RPM package License: GPL-3.0 URL: https://rustdesk.com Vendor: rustdesk -Requires: gtk3 libxcb libxdo libXfixes alsa-lib libvdpau libva pam gstreamer1-plugins-base +Requires: gtk3 libxcb libxdo libXfixes alsa-lib libva pam gstreamer1-plugins-base Recommends: libayatana-appindicator-gtk3 Provides: libdesktop_drop_plugin.so()(64bit), libdesktop_multi_window_plugin.so()(64bit), libfile_selector_linux_plugin.so()(64bit), libflutter_custom_cursor_plugin.so()(64bit), libflutter_linux_gtk.so()(64bit), libscreen_retriever_plugin.so()(64bit), libtray_manager_plugin.so()(64bit), liburl_launcher_linux_plugin.so()(64bit), libwindow_manager_plugin.so()(64bit), libwindow_size_plugin.so()(64bit), libtexture_rgba_renderer_plugin.so()(64bit) diff --git a/res/rpm-suse.spec b/res/rpm-suse.spec index 1d6a94b131bb..46710e3c9d17 100644 --- a/res/rpm-suse.spec +++ b/res/rpm-suse.spec @@ -3,7 +3,7 @@ Version: 1.1.9 Release: 0 Summary: RPM package License: GPL-3.0 -Requires: gtk3 libxcb1 xdotool libXfixes3 alsa-utils libXtst6 libvdpau1 libva2 pam gstreamer-plugins-base gstreamer-plugin-pipewire +Requires: gtk3 libxcb1 xdotool libXfixes3 alsa-utils libXtst6 libva2 pam gstreamer-plugins-base gstreamer-plugin-pipewire Recommends: libayatana-appindicator3-1 %description diff --git a/res/rpm.spec b/res/rpm.spec index 5f36a8b56005..1e8652140ae9 100644 --- a/res/rpm.spec +++ b/res/rpm.spec @@ -5,7 +5,7 @@ Summary: RPM package License: GPL-3.0 URL: https://rustdesk.com Vendor: rustdesk -Requires: gtk3 libxcb libxdo libXfixes alsa-lib libvdpau1 libva2 pam gstreamer1-plugins-base +Requires: gtk3 libxcb libxdo libXfixes alsa-lib libva2 pam gstreamer1-plugins-base Recommends: libayatana-appindicator-gtk3 %description diff --git a/res/vcpkg/ffmpeg/patch/0005-mediacodec-changing-bitrate.patch b/res/vcpkg/ffmpeg/patch/0005-mediacodec-changing-bitrate.patch index 1f70a5659308..1fb369b5ceaa 100644 --- a/res/vcpkg/ffmpeg/patch/0005-mediacodec-changing-bitrate.patch +++ b/res/vcpkg/ffmpeg/patch/0005-mediacodec-changing-bitrate.patch @@ -1,17 +1,17 @@ -From 51ac90d8084f7b153eac5133765fa9d0365aa239 Mon Sep 17 00:00:00 2001 +From ed73f8f6494d74ae47218f9503c7e3de385d9253 Mon Sep 17 00:00:00 2001 From: 21pages Date: Sun, 24 Nov 2024 14:17:39 +0800 -Subject: [PATCH 1/4] mediacodec changing bitrate +Subject: [PATCH 1/2] mediacodec changing bitrate Signed-off-by: 21pages --- - libavcodec/mediacodec_wrapper.c | 101 ++++++++++++++++++++++++++++++++ - libavcodec/mediacodec_wrapper.h | 7 +++ - libavcodec/mediacodecenc.c | 18 ++++++ - 3 files changed, 126 insertions(+) + libavcodec/mediacodec_wrapper.c | 97 +++++++++++++++++++++++++++++++++ + libavcodec/mediacodec_wrapper.h | 7 +++ + libavcodec/mediacodecenc.c | 18 ++++++ + 3 files changed, 122 insertions(+) diff --git a/libavcodec/mediacodec_wrapper.c b/libavcodec/mediacodec_wrapper.c -index 306359071e..1ab4e673f6 100644 +index 306359071e..7edb38a7d7 100644 --- a/libavcodec/mediacodec_wrapper.c +++ b/libavcodec/mediacodec_wrapper.c @@ -35,6 +35,8 @@ @@ -145,25 +145,7 @@ index 306359071e..1ab4e673f6 100644 }; typedef struct FFAMediaFormatNdk { -@@ -1893,6 +1982,8 @@ typedef struct FFAMediaCodecNdk { - // Available since API level 26. - media_status_t (*setInputSurface)(AMediaCodec*, ANativeWindow *); - media_status_t (*signalEndOfInputStream)(AMediaCodec *); -+ -+ media_status_t (*setParameters)(AMediaCodec *, const AMediaFormat *format); - } FFAMediaCodecNdk; - - static const FFAMediaFormat media_format_ndk; -@@ -2154,6 +2245,8 @@ static inline FFAMediaCodec *ndk_codec_create(int method, const char *arg) { - GET_SYMBOL(setInputSurface, 0) - GET_SYMBOL(signalEndOfInputStream, 0) - -+ GET_SYMBOL(setParameters, 0) -+ - #undef GET_SYMBOL - - switch (method) { -@@ -2428,6 +2521,12 @@ static int mediacodec_ndk_signalEndOfInputStream(FFAMediaCodec *ctx) +@@ -2428,6 +2517,12 @@ static int mediacodec_ndk_signalEndOfInputStream(FFAMediaCodec *ctx) return 0; } @@ -176,7 +158,7 @@ index 306359071e..1ab4e673f6 100644 static const FFAMediaFormat media_format_ndk = { .class = &amediaformat_ndk_class, -@@ -2489,6 +2588,8 @@ static const FFAMediaCodec media_codec_ndk = { +@@ -2489,6 +2584,8 @@ static const FFAMediaCodec media_codec_ndk = { .getConfigureFlagEncode = mediacodec_ndk_getConfigureFlagEncode, .cleanOutputBuffers = mediacodec_ndk_cleanOutputBuffers, .signalEndOfInputStream = mediacodec_ndk_signalEndOfInputStream, @@ -260,5 +242,5 @@ index d3bf27cb7f..621529d686 100644 // 1. Serious error // 2. Got a packet success -- -2.43.0.windows.1 +2.34.1 diff --git a/res/vcpkg/ffmpeg/patch/0006-dlopen-libva.patch b/res/vcpkg/ffmpeg/patch/0006-dlopen-libva.patch new file mode 100644 index 000000000000..e13a5de11e87 --- /dev/null +++ b/res/vcpkg/ffmpeg/patch/0006-dlopen-libva.patch @@ -0,0 +1,1993 @@ +From 6553fc4eae5d03bc712c30ae1e7519753c37275c Mon Sep 17 00:00:00 2001 +From: 21pages +Date: Wed, 4 Dec 2024 12:53:23 +0800 +Subject: [PATCH] dlopen libva + +Signed-off-by: 21pages +--- + libavcodec/vaapi_decode.c | 99 +++++++----- + libavcodec/vaapi_encode.c | 176 +++++++++++--------- + libavcodec/vaapi_encode_av1.c | 13 +- + libavcodec/vaapi_encode_h264.c | 3 +- + libavcodec/vaapi_encode_h265.c | 5 +- + libavutil/hwcontext_vaapi.c | 288 +++++++++++++++++++++++++-------- + libavutil/hwcontext_vaapi.h | 97 +++++++++++ + libavutil/hwcontext_vulkan.c | 5 +- + 8 files changed, 494 insertions(+), 192 deletions(-) + +diff --git a/libavcodec/vaapi_decode.c b/libavcodec/vaapi_decode.c +index cca94b5336..776270588f 100644 +--- a/libavcodec/vaapi_decode.c ++++ b/libavcodec/vaapi_decode.c +@@ -37,17 +37,18 @@ int ff_vaapi_decode_make_param_buffer(AVCodecContext *avctx, + size_t size) + { + VAAPIDecodeContext *ctx = avctx->internal->hwaccel_priv_data; ++ VAAPIDynLoadFunctions *vaf = ctx->hwctx->funcs; + VAStatus vas; + VABufferID buffer; + + av_assert0(pic->nb_param_buffers + 1 <= MAX_PARAM_BUFFERS); + +- vas = vaCreateBuffer(ctx->hwctx->display, ctx->va_context, ++ vas = vaf->vaCreateBuffer(ctx->hwctx->display, ctx->va_context, + type, size, 1, (void*)data, &buffer); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to create parameter " + "buffer (type %d): %d (%s).\n", +- type, vas, vaErrorStr(vas)); ++ type, vas, vaf->vaErrorStr(vas)); + return AVERROR(EIO); + } + +@@ -67,6 +68,7 @@ int ff_vaapi_decode_make_slice_buffer(AVCodecContext *avctx, + size_t slice_size) + { + VAAPIDecodeContext *ctx = avctx->internal->hwaccel_priv_data; ++ VAAPIDynLoadFunctions *vaf = ctx->hwctx->funcs; + VAStatus vas; + int index; + +@@ -85,13 +87,13 @@ int ff_vaapi_decode_make_slice_buffer(AVCodecContext *avctx, + + index = 2 * pic->nb_slices; + +- vas = vaCreateBuffer(ctx->hwctx->display, ctx->va_context, ++ vas = vaf->vaCreateBuffer(ctx->hwctx->display, ctx->va_context, + VASliceParameterBufferType, + params_size, 1, (void*)params_data, + &pic->slice_buffers[index]); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to create slice " +- "parameter buffer: %d (%s).\n", vas, vaErrorStr(vas)); ++ "parameter buffer: %d (%s).\n", vas, vaf->vaErrorStr(vas)); + return AVERROR(EIO); + } + +@@ -99,15 +101,15 @@ int ff_vaapi_decode_make_slice_buffer(AVCodecContext *avctx, + "is %#x.\n", pic->nb_slices, params_size, + pic->slice_buffers[index]); + +- vas = vaCreateBuffer(ctx->hwctx->display, ctx->va_context, ++ vas = vaf->vaCreateBuffer(ctx->hwctx->display, ctx->va_context, + VASliceDataBufferType, + slice_size, 1, (void*)slice_data, + &pic->slice_buffers[index + 1]); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to create slice " + "data buffer (size %zu): %d (%s).\n", +- slice_size, vas, vaErrorStr(vas)); +- vaDestroyBuffer(ctx->hwctx->display, ++ slice_size, vas, vaf->vaErrorStr(vas)); ++ vaf->vaDestroyBuffer(ctx->hwctx->display, + pic->slice_buffers[index]); + return AVERROR(EIO); + } +@@ -124,26 +126,27 @@ static void ff_vaapi_decode_destroy_buffers(AVCodecContext *avctx, + VAAPIDecodePicture *pic) + { + VAAPIDecodeContext *ctx = avctx->internal->hwaccel_priv_data; ++ VAAPIDynLoadFunctions *vaf = ctx->hwctx->funcs; + VAStatus vas; + int i; + + for (i = 0; i < pic->nb_param_buffers; i++) { +- vas = vaDestroyBuffer(ctx->hwctx->display, ++ vas = vaf->vaDestroyBuffer(ctx->hwctx->display, + pic->param_buffers[i]); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to destroy " + "parameter buffer %#x: %d (%s).\n", +- pic->param_buffers[i], vas, vaErrorStr(vas)); ++ pic->param_buffers[i], vas, vaf->vaErrorStr(vas)); + } + } + + for (i = 0; i < 2 * pic->nb_slices; i++) { +- vas = vaDestroyBuffer(ctx->hwctx->display, ++ vas = vaf->vaDestroyBuffer(ctx->hwctx->display, + pic->slice_buffers[i]); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to destroy slice " + "slice buffer %#x: %d (%s).\n", +- pic->slice_buffers[i], vas, vaErrorStr(vas)); ++ pic->slice_buffers[i], vas, vaf->vaErrorStr(vas)); + } + } + } +@@ -152,43 +155,44 @@ int ff_vaapi_decode_issue(AVCodecContext *avctx, + VAAPIDecodePicture *pic) + { + VAAPIDecodeContext *ctx = avctx->internal->hwaccel_priv_data; ++ VAAPIDynLoadFunctions *vaf = ctx->hwctx->funcs; + VAStatus vas; + int err; + + av_log(avctx, AV_LOG_DEBUG, "Decode to surface %#x.\n", + pic->output_surface); + +- vas = vaBeginPicture(ctx->hwctx->display, ctx->va_context, ++ vas = vaf->vaBeginPicture(ctx->hwctx->display, ctx->va_context, + pic->output_surface); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to begin picture decode " +- "issue: %d (%s).\n", vas, vaErrorStr(vas)); ++ "issue: %d (%s).\n", vas, vaf->vaErrorStr(vas)); + err = AVERROR(EIO); + goto fail_with_picture; + } + +- vas = vaRenderPicture(ctx->hwctx->display, ctx->va_context, ++ vas = vaf->vaRenderPicture(ctx->hwctx->display, ctx->va_context, + pic->param_buffers, pic->nb_param_buffers); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to upload decode " +- "parameters: %d (%s).\n", vas, vaErrorStr(vas)); ++ "parameters: %d (%s).\n", vas, vaf->vaErrorStr(vas)); + err = AVERROR(EIO); + goto fail_with_picture; + } + +- vas = vaRenderPicture(ctx->hwctx->display, ctx->va_context, ++ vas = vaf->vaRenderPicture(ctx->hwctx->display, ctx->va_context, + pic->slice_buffers, 2 * pic->nb_slices); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to upload slices: " +- "%d (%s).\n", vas, vaErrorStr(vas)); ++ "%d (%s).\n", vas, vaf->vaErrorStr(vas)); + err = AVERROR(EIO); + goto fail_with_picture; + } + +- vas = vaEndPicture(ctx->hwctx->display, ctx->va_context); ++ vas = vaf->vaEndPicture(ctx->hwctx->display, ctx->va_context); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to end picture decode " +- "issue: %d (%s).\n", vas, vaErrorStr(vas)); ++ "issue: %d (%s).\n", vas, vaf->vaErrorStr(vas)); + err = AVERROR(EIO); + if (CONFIG_VAAPI_1 || ctx->hwctx->driver_quirks & + AV_VAAPI_DRIVER_QUIRK_RENDER_PARAM_BUFFERS) +@@ -205,10 +209,10 @@ int ff_vaapi_decode_issue(AVCodecContext *avctx, + goto exit; + + fail_with_picture: +- vas = vaEndPicture(ctx->hwctx->display, ctx->va_context); ++ vas = vaf->vaEndPicture(ctx->hwctx->display, ctx->va_context); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to end picture decode " +- "after error: %d (%s).\n", vas, vaErrorStr(vas)); ++ "after error: %d (%s).\n", vas, vaf->vaErrorStr(vas)); + } + fail: + ff_vaapi_decode_destroy_buffers(avctx, pic); +@@ -296,6 +300,7 @@ static int vaapi_decode_find_best_format(AVCodecContext *avctx, + AVHWFramesContext *frames) + { + AVVAAPIDeviceContext *hwctx = device->hwctx; ++ VAAPIDynLoadFunctions *vaf = hwctx->funcs; + VAStatus vas; + VASurfaceAttrib *attr; + enum AVPixelFormat source_format, best_format, format; +@@ -305,11 +310,11 @@ static int vaapi_decode_find_best_format(AVCodecContext *avctx, + source_format = avctx->sw_pix_fmt; + av_assert0(source_format != AV_PIX_FMT_NONE); + +- vas = vaQuerySurfaceAttributes(hwctx->display, config_id, ++ vas = vaf->vaQuerySurfaceAttributes(hwctx->display, config_id, + NULL, &nb_attr); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to query surface attributes: " +- "%d (%s).\n", vas, vaErrorStr(vas)); ++ "%d (%s).\n", vas, vaf->vaErrorStr(vas)); + return AVERROR(ENOSYS); + } + +@@ -317,11 +322,11 @@ static int vaapi_decode_find_best_format(AVCodecContext *avctx, + if (!attr) + return AVERROR(ENOMEM); + +- vas = vaQuerySurfaceAttributes(hwctx->display, config_id, ++ vas = vaf->vaQuerySurfaceAttributes(hwctx->display, config_id, + attr, &nb_attr); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to query surface attributes: " +- "%d (%s).\n", vas, vaErrorStr(vas)); ++ "%d (%s).\n", vas, vaf->vaErrorStr(vas)); + av_freep(&attr); + return AVERROR(ENOSYS); + } +@@ -463,6 +468,7 @@ static int vaapi_decode_make_config(AVCodecContext *avctx, + + AVHWDeviceContext *device = (AVHWDeviceContext*)device_ref->data; + AVVAAPIDeviceContext *hwctx = device->hwctx; ++ VAAPIDynLoadFunctions *vaf = hwctx->funcs; + + codec_desc = avcodec_descriptor_get(avctx->codec_id); + if (!codec_desc) { +@@ -470,7 +476,7 @@ static int vaapi_decode_make_config(AVCodecContext *avctx, + goto fail; + } + +- profile_count = vaMaxNumProfiles(hwctx->display); ++ profile_count = vaf->vaMaxNumProfiles(hwctx->display); + profile_list = av_malloc_array(profile_count, + sizeof(VAProfile)); + if (!profile_list) { +@@ -478,11 +484,11 @@ static int vaapi_decode_make_config(AVCodecContext *avctx, + goto fail; + } + +- vas = vaQueryConfigProfiles(hwctx->display, ++ vas = vaf->vaQueryConfigProfiles(hwctx->display, + profile_list, &profile_count); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to query profiles: " +- "%d (%s).\n", vas, vaErrorStr(vas)); ++ "%d (%s).\n", vas, vaf->vaErrorStr(vas)); + err = AVERROR(ENOSYS); + goto fail; + } +@@ -542,12 +548,12 @@ static int vaapi_decode_make_config(AVCodecContext *avctx, + } + } + +- vas = vaCreateConfig(hwctx->display, matched_va_profile, ++ vas = vaf->vaCreateConfig(hwctx->display, matched_va_profile, + VAEntrypointVLD, NULL, 0, + va_config); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to create decode " +- "configuration: %d (%s).\n", vas, vaErrorStr(vas)); ++ "configuration: %d (%s).\n", vas, vaf->vaErrorStr(vas)); + err = AVERROR(EIO); + goto fail; + } +@@ -626,7 +632,7 @@ fail: + av_hwframe_constraints_free(&constraints); + av_freep(&hwconfig); + if (*va_config != VA_INVALID_ID) { +- vaDestroyConfig(hwctx->display, *va_config); ++ vaf->vaDestroyConfig(hwctx->display, *va_config); + *va_config = VA_INVALID_ID; + } + av_freep(&profile_list); +@@ -639,20 +645,21 @@ int ff_vaapi_common_frame_params(AVCodecContext *avctx, + AVHWFramesContext *hw_frames = (AVHWFramesContext *)hw_frames_ctx->data; + AVHWDeviceContext *device_ctx = hw_frames->device_ctx; + AVVAAPIDeviceContext *hwctx; ++ VAAPIDynLoadFunctions *vaf; + VAConfigID va_config = VA_INVALID_ID; + int err; + + if (device_ctx->type != AV_HWDEVICE_TYPE_VAAPI) + return AVERROR(EINVAL); + hwctx = device_ctx->hwctx; +- ++ vaf = hwctx->funcs; + err = vaapi_decode_make_config(avctx, hw_frames->device_ref, &va_config, + hw_frames_ctx); + if (err) + return err; + + if (va_config != VA_INVALID_ID) +- vaDestroyConfig(hwctx->display, va_config); ++ vaf->vaDestroyConfig(hwctx->display, va_config); + + return 0; + } +@@ -660,6 +667,7 @@ int ff_vaapi_common_frame_params(AVCodecContext *avctx, + int ff_vaapi_decode_init(AVCodecContext *avctx) + { + VAAPIDecodeContext *ctx = avctx->internal->hwaccel_priv_data; ++ VAAPIDynLoadFunctions *vaf; + VAStatus vas; + int err; + +@@ -674,13 +682,17 @@ int ff_vaapi_decode_init(AVCodecContext *avctx) + ctx->hwfc = ctx->frames->hwctx; + ctx->device = ctx->frames->device_ctx; + ctx->hwctx = ctx->device->hwctx; +- ++ if (!ctx->hwctx || !ctx->hwctx->funcs) { ++ err = AVERROR(EINVAL); ++ goto fail; ++ } ++ vaf = ctx->hwctx->funcs; + err = vaapi_decode_make_config(avctx, ctx->frames->device_ref, + &ctx->va_config, NULL); + if (err) + goto fail; + +- vas = vaCreateContext(ctx->hwctx->display, ctx->va_config, ++ vas = vaf->vaCreateContext(ctx->hwctx->display, ctx->va_config, + avctx->coded_width, avctx->coded_height, + VA_PROGRESSIVE, + ctx->hwfc->surface_ids, +@@ -688,7 +700,7 @@ int ff_vaapi_decode_init(AVCodecContext *avctx) + &ctx->va_context); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to create decode " +- "context: %d (%s).\n", vas, vaErrorStr(vas)); ++ "context: %d (%s).\n", vas, vaf->vaErrorStr(vas)); + err = AVERROR(EIO); + goto fail; + } +@@ -706,22 +718,29 @@ fail: + int ff_vaapi_decode_uninit(AVCodecContext *avctx) + { + VAAPIDecodeContext *ctx = avctx->internal->hwaccel_priv_data; ++ VAAPIDynLoadFunctions *vaf = NULL; + VAStatus vas; + ++ if (ctx->hwctx && ctx->hwctx->funcs) ++ vaf = ctx->hwctx->funcs; ++ ++ if (!vaf) ++ return 0; ++ + if (ctx->va_context != VA_INVALID_ID) { +- vas = vaDestroyContext(ctx->hwctx->display, ctx->va_context); ++ vas = vaf->vaDestroyContext(ctx->hwctx->display, ctx->va_context); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to destroy decode " + "context %#x: %d (%s).\n", +- ctx->va_context, vas, vaErrorStr(vas)); ++ ctx->va_context, vas, vaf->vaErrorStr(vas)); + } + } + if (ctx->va_config != VA_INVALID_ID) { +- vas = vaDestroyConfig(ctx->hwctx->display, ctx->va_config); ++ vas = vaf->vaDestroyConfig(ctx->hwctx->display, ctx->va_config); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to destroy decode " + "configuration %#x: %d (%s).\n", +- ctx->va_config, vas, vaErrorStr(vas)); ++ ctx->va_config, vas, vaf->vaErrorStr(vas)); + } + } + +diff --git a/libavcodec/vaapi_encode.c b/libavcodec/vaapi_encode.c +index b8765a19c7..65eb8740a8 100644 +--- a/libavcodec/vaapi_encode.c ++++ b/libavcodec/vaapi_encode.c +@@ -44,6 +44,7 @@ static int vaapi_encode_make_packed_header(AVCodecContext *avctx, + int type, char *data, size_t bit_len) + { + VAAPIEncodeContext *ctx = avctx->priv_data; ++ VAAPIDynLoadFunctions *vaf = ctx->hwctx->funcs; + VAStatus vas; + VABufferID param_buffer, data_buffer; + VABufferID *tmp; +@@ -58,24 +59,24 @@ static int vaapi_encode_make_packed_header(AVCodecContext *avctx, + return AVERROR(ENOMEM); + pic->param_buffers = tmp; + +- vas = vaCreateBuffer(ctx->hwctx->display, ctx->va_context, ++ vas = vaf->vaCreateBuffer(ctx->hwctx->display, ctx->va_context, + VAEncPackedHeaderParameterBufferType, + sizeof(params), 1, ¶ms, ¶m_buffer); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to create parameter buffer " + "for packed header (type %d): %d (%s).\n", +- type, vas, vaErrorStr(vas)); ++ type, vas, vaf->vaErrorStr(vas)); + return AVERROR(EIO); + } + pic->param_buffers[pic->nb_param_buffers++] = param_buffer; + +- vas = vaCreateBuffer(ctx->hwctx->display, ctx->va_context, ++ vas = vaf->vaCreateBuffer(ctx->hwctx->display, ctx->va_context, + VAEncPackedHeaderDataBufferType, + (bit_len + 7) / 8, 1, data, &data_buffer); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to create data buffer " + "for packed header (type %d): %d (%s).\n", +- type, vas, vaErrorStr(vas)); ++ type, vas, vaf->vaErrorStr(vas)); + return AVERROR(EIO); + } + pic->param_buffers[pic->nb_param_buffers++] = data_buffer; +@@ -90,6 +91,7 @@ static int vaapi_encode_make_param_buffer(AVCodecContext *avctx, + int type, char *data, size_t len) + { + VAAPIEncodeContext *ctx = avctx->priv_data; ++ VAAPIDynLoadFunctions *vaf = ctx->hwctx->funcs; + VAStatus vas; + VABufferID *tmp; + VABufferID buffer; +@@ -99,11 +101,11 @@ static int vaapi_encode_make_param_buffer(AVCodecContext *avctx, + return AVERROR(ENOMEM); + pic->param_buffers = tmp; + +- vas = vaCreateBuffer(ctx->hwctx->display, ctx->va_context, +- type, len, 1, data, &buffer); ++ vas = vaf->vaCreateBuffer(ctx->hwctx->display, ctx->va_context, ++ type, len, 1, data, &buffer); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to create parameter buffer " +- "(type %d): %d (%s).\n", type, vas, vaErrorStr(vas)); ++ "(type %d): %d (%s).\n", type, vas, vaf->vaErrorStr(vas)); + return AVERROR(EIO); + } + pic->param_buffers[pic->nb_param_buffers++] = buffer; +@@ -140,6 +142,7 @@ static int vaapi_encode_wait(AVCodecContext *avctx, + VAAPIEncodePicture *pic) + { + VAAPIEncodeContext *ctx = avctx->priv_data; ++ VAAPIDynLoadFunctions *vaf = ctx->hwctx->funcs; + VAStatus vas; + + av_assert0(pic->encode_issued); +@@ -154,22 +157,22 @@ static int vaapi_encode_wait(AVCodecContext *avctx, + pic->encode_order, pic->input_surface); + + #if VA_CHECK_VERSION(1, 9, 0) +- if (ctx->has_sync_buffer_func) { +- vas = vaSyncBuffer(ctx->hwctx->display, ++ if (ctx->has_sync_buffer_func && vaf->vaSyncBuffer) { ++ vas = vaf->vaSyncBuffer(ctx->hwctx->display, + pic->output_buffer, + VA_TIMEOUT_INFINITE); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to sync to output buffer completion: " +- "%d (%s).\n", vas, vaErrorStr(vas)); ++ "%d (%s).\n", vas, vaf->vaErrorStr(vas)); + return AVERROR(EIO); + } + } else + #endif + { // If vaSyncBuffer is not implemented, try old version API. +- vas = vaSyncSurface(ctx->hwctx->display, pic->input_surface); ++ vas = vaf->vaSyncSurface(ctx->hwctx->display, pic->input_surface); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to sync to picture completion: " +- "%d (%s).\n", vas, vaErrorStr(vas)); ++ "%d (%s).\n", vas, vaf->vaErrorStr(vas)); + return AVERROR(EIO); + } + } +@@ -267,6 +270,7 @@ static int vaapi_encode_issue(AVCodecContext *avctx, + VAAPIEncodePicture *pic) + { + VAAPIEncodeContext *ctx = avctx->priv_data; ++ VAAPIDynLoadFunctions *vaf = ctx->hwctx->funcs; + VAAPIEncodeSlice *slice; + VAStatus vas; + int err, i; +@@ -594,28 +598,28 @@ static int vaapi_encode_issue(AVCodecContext *avctx, + } + #endif + +- vas = vaBeginPicture(ctx->hwctx->display, ctx->va_context, ++ vas = vaf->vaBeginPicture(ctx->hwctx->display, ctx->va_context, + pic->input_surface); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to begin picture encode issue: " +- "%d (%s).\n", vas, vaErrorStr(vas)); ++ "%d (%s).\n", vas, vaf->vaErrorStr(vas)); + err = AVERROR(EIO); + goto fail_with_picture; + } + +- vas = vaRenderPicture(ctx->hwctx->display, ctx->va_context, ++ vas = vaf->vaRenderPicture(ctx->hwctx->display, ctx->va_context, + pic->param_buffers, pic->nb_param_buffers); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to upload encode parameters: " +- "%d (%s).\n", vas, vaErrorStr(vas)); ++ "%d (%s).\n", vas, vaf->vaErrorStr(vas)); + err = AVERROR(EIO); + goto fail_with_picture; + } + +- vas = vaEndPicture(ctx->hwctx->display, ctx->va_context); ++ vas = vaf->vaEndPicture(ctx->hwctx->display, ctx->va_context); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to end picture encode issue: " +- "%d (%s).\n", vas, vaErrorStr(vas)); ++ "%d (%s).\n", vas, vaf->vaErrorStr(vas)); + err = AVERROR(EIO); + // vaRenderPicture() has been called here, so we should not destroy + // the parameter buffers unless separate destruction is required. +@@ -629,12 +633,12 @@ static int vaapi_encode_issue(AVCodecContext *avctx, + if (CONFIG_VAAPI_1 || ctx->hwctx->driver_quirks & + AV_VAAPI_DRIVER_QUIRK_RENDER_PARAM_BUFFERS) { + for (i = 0; i < pic->nb_param_buffers; i++) { +- vas = vaDestroyBuffer(ctx->hwctx->display, ++ vas = vaf->vaDestroyBuffer(ctx->hwctx->display, + pic->param_buffers[i]); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to destroy " + "param buffer %#x: %d (%s).\n", +- pic->param_buffers[i], vas, vaErrorStr(vas)); ++ pic->param_buffers[i], vas, vaf->vaErrorStr(vas)); + // And ignore. + } + } +@@ -645,10 +649,10 @@ static int vaapi_encode_issue(AVCodecContext *avctx, + return 0; + + fail_with_picture: +- vaEndPicture(ctx->hwctx->display, ctx->va_context); ++ vaf->vaEndPicture(ctx->hwctx->display, ctx->va_context); + fail: + for(i = 0; i < pic->nb_param_buffers; i++) +- vaDestroyBuffer(ctx->hwctx->display, pic->param_buffers[i]); ++ vaf->vaDestroyBuffer(ctx->hwctx->display, pic->param_buffers[i]); + if (pic->slices) { + for (i = 0; i < pic->nb_slices; i++) + av_freep(&pic->slices[i].codec_slice_params); +@@ -707,16 +711,17 @@ static int vaapi_encode_set_output_property(AVCodecContext *avctx, + static int vaapi_encode_get_coded_buffer_size(AVCodecContext *avctx, VABufferID buf_id) + { + VAAPIEncodeContext *ctx = avctx->priv_data; ++ VAAPIDynLoadFunctions *vaf = ctx->hwctx->funcs; + VACodedBufferSegment *buf_list, *buf; + int size = 0; + VAStatus vas; + int err; + +- vas = vaMapBuffer(ctx->hwctx->display, buf_id, ++ vas = vaf->vaMapBuffer(ctx->hwctx->display, buf_id, + (void**)&buf_list); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to map output buffers: " +- "%d (%s).\n", vas, vaErrorStr(vas)); ++ "%d (%s).\n", vas, vaf->vaErrorStr(vas)); + err = AVERROR(EIO); + return err; + } +@@ -724,10 +729,10 @@ static int vaapi_encode_get_coded_buffer_size(AVCodecContext *avctx, VABufferID + for (buf = buf_list; buf; buf = buf->next) + size += buf->size; + +- vas = vaUnmapBuffer(ctx->hwctx->display, buf_id); ++ vas = vaf->vaUnmapBuffer(ctx->hwctx->display, buf_id); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to unmap output buffers: " +- "%d (%s).\n", vas, vaErrorStr(vas)); ++ "%d (%s).\n", vas, vaf->vaErrorStr(vas)); + err = AVERROR(EIO); + return err; + } +@@ -739,15 +744,16 @@ static int vaapi_encode_get_coded_buffer_data(AVCodecContext *avctx, + VABufferID buf_id, uint8_t **dst) + { + VAAPIEncodeContext *ctx = avctx->priv_data; ++ VAAPIDynLoadFunctions *vaf = ctx->hwctx->funcs; + VACodedBufferSegment *buf_list, *buf; + VAStatus vas; + int err; + +- vas = vaMapBuffer(ctx->hwctx->display, buf_id, ++ vas = vaf->vaMapBuffer(ctx->hwctx->display, buf_id, + (void**)&buf_list); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to map output buffers: " +- "%d (%s).\n", vas, vaErrorStr(vas)); ++ "%d (%s).\n", vas, vaf->vaErrorStr(vas)); + err = AVERROR(EIO); + return err; + } +@@ -760,10 +766,10 @@ static int vaapi_encode_get_coded_buffer_data(AVCodecContext *avctx, + *dst += buf->size; + } + +- vas = vaUnmapBuffer(ctx->hwctx->display, buf_id); ++ vas = vaf->vaUnmapBuffer(ctx->hwctx->display, buf_id); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to unmap output buffers: " +- "%d (%s).\n", vas, vaErrorStr(vas)); ++ "%d (%s).\n", vas, vaf->vaErrorStr(vas)); + err = AVERROR(EIO); + return err; + } +@@ -1552,6 +1558,7 @@ static const VAEntrypoint vaapi_encode_entrypoints_low_power[] = { + static av_cold int vaapi_encode_profile_entrypoint(AVCodecContext *avctx) + { + VAAPIEncodeContext *ctx = avctx->priv_data; ++ VAAPIDynLoadFunctions *vaf = ctx->hwctx->funcs; + VAProfile *va_profiles = NULL; + VAEntrypoint *va_entrypoints = NULL; + VAStatus vas; +@@ -1593,16 +1600,16 @@ static av_cold int vaapi_encode_profile_entrypoint(AVCodecContext *avctx) + av_log(avctx, AV_LOG_VERBOSE, "Input surface format is %s.\n", + desc->name); + +- n = vaMaxNumProfiles(ctx->hwctx->display); ++ n = vaf->vaMaxNumProfiles(ctx->hwctx->display); + va_profiles = av_malloc_array(n, sizeof(VAProfile)); + if (!va_profiles) { + err = AVERROR(ENOMEM); + goto fail; + } +- vas = vaQueryConfigProfiles(ctx->hwctx->display, va_profiles, &n); ++ vas = vaf->vaQueryConfigProfiles(ctx->hwctx->display, va_profiles, &n); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to query profiles: %d (%s).\n", +- vas, vaErrorStr(vas)); ++ vas, vaf->vaErrorStr(vas)); + err = AVERROR_EXTERNAL; + goto fail; + } +@@ -1623,7 +1630,7 @@ static av_cold int vaapi_encode_profile_entrypoint(AVCodecContext *avctx) + continue; + + #if VA_CHECK_VERSION(1, 0, 0) +- profile_string = vaProfileStr(profile->va_profile); ++ profile_string = vaf->vaProfileStr(profile->va_profile); + #else + profile_string = "(no profile names)"; + #endif +@@ -1653,18 +1660,18 @@ static av_cold int vaapi_encode_profile_entrypoint(AVCodecContext *avctx) + av_log(avctx, AV_LOG_VERBOSE, "Using VAAPI profile %s (%d).\n", + profile_string, ctx->va_profile); + +- n = vaMaxNumEntrypoints(ctx->hwctx->display); ++ n = vaf->vaMaxNumEntrypoints(ctx->hwctx->display); + va_entrypoints = av_malloc_array(n, sizeof(VAEntrypoint)); + if (!va_entrypoints) { + err = AVERROR(ENOMEM); + goto fail; + } +- vas = vaQueryConfigEntrypoints(ctx->hwctx->display, ctx->va_profile, ++ vas = vaf->vaQueryConfigEntrypoints(ctx->hwctx->display, ctx->va_profile, + va_entrypoints, &n); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to query entrypoints for " + "profile %s (%d): %d (%s).\n", profile_string, +- ctx->va_profile, vas, vaErrorStr(vas)); ++ ctx->va_profile, vas, vaf->vaErrorStr(vas)); + err = AVERROR_EXTERNAL; + goto fail; + } +@@ -1686,7 +1693,7 @@ static av_cold int vaapi_encode_profile_entrypoint(AVCodecContext *avctx) + + ctx->va_entrypoint = va_entrypoints[i]; + #if VA_CHECK_VERSION(1, 0, 0) +- entrypoint_string = vaEntrypointStr(ctx->va_entrypoint); ++ entrypoint_string = vaf->vaEntrypointStr(ctx->va_entrypoint); + #else + entrypoint_string = "(no entrypoint names)"; + #endif +@@ -1711,12 +1718,12 @@ static av_cold int vaapi_encode_profile_entrypoint(AVCodecContext *avctx) + } + + rt_format_attr = (VAConfigAttrib) { VAConfigAttribRTFormat }; +- vas = vaGetConfigAttributes(ctx->hwctx->display, ++ vas = vaf->vaGetConfigAttributes(ctx->hwctx->display, + ctx->va_profile, ctx->va_entrypoint, + &rt_format_attr, 1); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to query RT format " +- "config attribute: %d (%s).\n", vas, vaErrorStr(vas)); ++ "config attribute: %d (%s).\n", vas, vaf->vaErrorStr(vas)); + err = AVERROR_EXTERNAL; + goto fail; + } +@@ -1773,6 +1780,7 @@ static const VAAPIEncodeRCMode vaapi_encode_rc_modes[] = { + static av_cold int vaapi_encode_init_rate_control(AVCodecContext *avctx) + { + VAAPIEncodeContext *ctx = avctx->priv_data; ++ VAAPIDynLoadFunctions *vaf = ctx->hwctx->funcs; + uint32_t supported_va_rc_modes; + const VAAPIEncodeRCMode *rc_mode; + int64_t rc_bits_per_second; +@@ -1786,12 +1794,12 @@ static av_cold int vaapi_encode_init_rate_control(AVCodecContext *avctx) + VAStatus vas; + char supported_rc_modes_string[64]; + +- vas = vaGetConfigAttributes(ctx->hwctx->display, ++ vas = vaf->vaGetConfigAttributes(ctx->hwctx->display, + ctx->va_profile, ctx->va_entrypoint, + &rc_attr, 1); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to query rate control " +- "config attribute: %d (%s).\n", vas, vaErrorStr(vas)); ++ "config attribute: %d (%s).\n", vas, vaf->vaErrorStr(vas)); + return AVERROR_EXTERNAL; + } + if (rc_attr.value == VA_ATTRIB_NOT_SUPPORTED) { +@@ -2132,6 +2140,7 @@ static av_cold int vaapi_encode_init_max_frame_size(AVCodecContext *avctx) + { + #if VA_CHECK_VERSION(1, 5, 0) + VAAPIEncodeContext *ctx = avctx->priv_data; ++ VAAPIDynLoadFunctions *vaf = ctx->hwctx->funcs; + VAConfigAttrib attr = { VAConfigAttribMaxFrameSize }; + VAStatus vas; + +@@ -2142,14 +2151,14 @@ static av_cold int vaapi_encode_init_max_frame_size(AVCodecContext *avctx) + return AVERROR(EINVAL); + } + +- vas = vaGetConfigAttributes(ctx->hwctx->display, ++ vas = vaf->vaGetConfigAttributes(ctx->hwctx->display, + ctx->va_profile, + ctx->va_entrypoint, + &attr, 1); + if (vas != VA_STATUS_SUCCESS) { + ctx->max_frame_size = 0; + av_log(avctx, AV_LOG_ERROR, "Failed to query max frame size " +- "config attribute: %d (%s).\n", vas, vaErrorStr(vas)); ++ "config attribute: %d (%s).\n", vas, vaf->vaErrorStr(vas)); + return AVERROR_EXTERNAL; + } + +@@ -2188,18 +2197,19 @@ static av_cold int vaapi_encode_init_max_frame_size(AVCodecContext *avctx) + static av_cold int vaapi_encode_init_gop_structure(AVCodecContext *avctx) + { + VAAPIEncodeContext *ctx = avctx->priv_data; ++ VAAPIDynLoadFunctions *vaf = ctx->hwctx->funcs; + VAStatus vas; + VAConfigAttrib attr = { VAConfigAttribEncMaxRefFrames }; + uint32_t ref_l0, ref_l1; + int prediction_pre_only; + +- vas = vaGetConfigAttributes(ctx->hwctx->display, ++ vas = vaf->vaGetConfigAttributes(ctx->hwctx->display, + ctx->va_profile, + ctx->va_entrypoint, + &attr, 1); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to query reference frames " +- "attribute: %d (%s).\n", vas, vaErrorStr(vas)); ++ "attribute: %d (%s).\n", vas, vaf->vaErrorStr(vas)); + return AVERROR_EXTERNAL; + } + +@@ -2217,13 +2227,13 @@ static av_cold int vaapi_encode_init_gop_structure(AVCodecContext *avctx) + if (!(ctx->codec->flags & FLAG_INTRA_ONLY || + avctx->gop_size <= 1)) { + attr = (VAConfigAttrib) { VAConfigAttribPredictionDirection }; +- vas = vaGetConfigAttributes(ctx->hwctx->display, ++ vas = vaf->vaGetConfigAttributes(ctx->hwctx->display, + ctx->va_profile, + ctx->va_entrypoint, + &attr, 1); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_WARNING, "Failed to query prediction direction " +- "attribute: %d (%s).\n", vas, vaErrorStr(vas)); ++ "attribute: %d (%s).\n", vas, vaf->vaErrorStr(vas)); + return AVERROR_EXTERNAL; + } else if (attr.value == VA_ATTRIB_NOT_SUPPORTED) { + av_log(avctx, AV_LOG_VERBOSE, "Driver does not report any additional " +@@ -2409,12 +2419,14 @@ static av_cold int vaapi_encode_init_tile_slice_structure(AVCodecContext *avctx, + av_log(avctx, AV_LOG_VERBOSE, "Encoding pictures with %d x %d tile.\n", + ctx->tile_rows, ctx->tile_cols); + ++ + return 0; + } + + static av_cold int vaapi_encode_init_slice_structure(AVCodecContext *avctx) + { + VAAPIEncodeContext *ctx = avctx->priv_data; ++ VAAPIDynLoadFunctions *vaf = ctx->hwctx->funcs; + VAConfigAttrib attr[3] = { { VAConfigAttribEncMaxSlices }, + { VAConfigAttribEncSliceStructure }, + #if VA_CHECK_VERSION(1, 1, 0) +@@ -2446,13 +2458,13 @@ static av_cold int vaapi_encode_init_slice_structure(AVCodecContext *avctx) + return 0; + } + +- vas = vaGetConfigAttributes(ctx->hwctx->display, ++ vas = vaf->vaGetConfigAttributes(ctx->hwctx->display, + ctx->va_profile, + ctx->va_entrypoint, + attr, FF_ARRAY_ELEMS(attr)); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to query slice " +- "attributes: %d (%s).\n", vas, vaErrorStr(vas)); ++ "attributes: %d (%s).\n", vas, vaf->vaErrorStr(vas)); + return AVERROR_EXTERNAL; + } + max_slices = attr[0].value; +@@ -2506,16 +2518,17 @@ static av_cold int vaapi_encode_init_slice_structure(AVCodecContext *avctx) + static av_cold int vaapi_encode_init_packed_headers(AVCodecContext *avctx) + { + VAAPIEncodeContext *ctx = avctx->priv_data; ++ VAAPIDynLoadFunctions *vaf = ctx->hwctx->funcs; + VAStatus vas; + VAConfigAttrib attr = { VAConfigAttribEncPackedHeaders }; + +- vas = vaGetConfigAttributes(ctx->hwctx->display, ++ vas = vaf->vaGetConfigAttributes(ctx->hwctx->display, + ctx->va_profile, + ctx->va_entrypoint, + &attr, 1); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to query packed headers " +- "attribute: %d (%s).\n", vas, vaErrorStr(vas)); ++ "attribute: %d (%s).\n", vas, vaf->vaErrorStr(vas)); + return AVERROR_EXTERNAL; + } + +@@ -2567,17 +2580,18 @@ static av_cold int vaapi_encode_init_quality(AVCodecContext *avctx) + { + #if VA_CHECK_VERSION(0, 36, 0) + VAAPIEncodeContext *ctx = avctx->priv_data; ++ VAAPIDynLoadFunctions *vaf = ctx->hwctx->funcs; + VAStatus vas; + VAConfigAttrib attr = { VAConfigAttribEncQualityRange }; + int quality = avctx->compression_level; + +- vas = vaGetConfigAttributes(ctx->hwctx->display, ++ vas = vaf->vaGetConfigAttributes(ctx->hwctx->display, + ctx->va_profile, + ctx->va_entrypoint, + &attr, 1); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to query quality " +- "config attribute: %d (%s).\n", vas, vaErrorStr(vas)); ++ "config attribute: %d (%s).\n", vas, vaf->vaErrorStr(vas)); + return AVERROR_EXTERNAL; + } + +@@ -2614,16 +2628,17 @@ static av_cold int vaapi_encode_init_roi(AVCodecContext *avctx) + { + #if VA_CHECK_VERSION(1, 0, 0) + VAAPIEncodeContext *ctx = avctx->priv_data; ++ VAAPIDynLoadFunctions *vaf = ctx->hwctx->funcs; + VAStatus vas; + VAConfigAttrib attr = { VAConfigAttribEncROI }; + +- vas = vaGetConfigAttributes(ctx->hwctx->display, ++ vas = vaf->vaGetConfigAttributes(ctx->hwctx->display, + ctx->va_profile, + ctx->va_entrypoint, + &attr, 1); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to query ROI " +- "config attribute: %d (%s).\n", vas, vaErrorStr(vas)); ++ "config attribute: %d (%s).\n", vas, vaf->vaErrorStr(vas)); + return AVERROR_EXTERNAL; + } + +@@ -2648,10 +2663,11 @@ static void vaapi_encode_free_output_buffer(FFRefStructOpaque opaque, + { + AVCodecContext *avctx = opaque.nc; + VAAPIEncodeContext *ctx = avctx->priv_data; ++ VAAPIDynLoadFunctions *vaf = ctx->hwctx->funcs; + VABufferID *buffer_id_ref = obj; + VABufferID buffer_id = *buffer_id_ref; + +- vaDestroyBuffer(ctx->hwctx->display, buffer_id); ++ vaf->vaDestroyBuffer(ctx->hwctx->display, buffer_id); + + av_log(avctx, AV_LOG_DEBUG, "Freed output buffer %#x\n", buffer_id); + } +@@ -2660,6 +2676,7 @@ static int vaapi_encode_alloc_output_buffer(FFRefStructOpaque opaque, void *obj) + { + AVCodecContext *avctx = opaque.nc; + VAAPIEncodeContext *ctx = avctx->priv_data; ++ VAAPIDynLoadFunctions *vaf = ctx->hwctx->funcs; + VABufferID *buffer_id = obj; + VAStatus vas; + +@@ -2667,13 +2684,13 @@ static int vaapi_encode_alloc_output_buffer(FFRefStructOpaque opaque, void *obj) + // to hold the largest possible compressed frame. We assume here + // that the uncompressed frame plus some header data is an upper + // bound on that. +- vas = vaCreateBuffer(ctx->hwctx->display, ctx->va_context, ++ vas = vaf->vaCreateBuffer(ctx->hwctx->display, ctx->va_context, + VAEncCodedBufferType, + 3 * ctx->surface_width * ctx->surface_height + + (1 << 16), 1, 0, buffer_id); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to create bitstream " +- "output buffer: %d (%s).\n", vas, vaErrorStr(vas)); ++ "output buffer: %d (%s).\n", vas, vaf->vaErrorStr(vas)); + return AVERROR(ENOMEM); + } + +@@ -2773,6 +2790,7 @@ static av_cold int vaapi_encode_create_recon_frames(AVCodecContext *avctx) + av_cold int ff_vaapi_encode_init(AVCodecContext *avctx) + { + VAAPIEncodeContext *ctx = avctx->priv_data; ++ VAAPIDynLoadFunctions *vaf = NULL; + AVVAAPIFramesContext *recon_hwctx = NULL; + VAStatus vas; + int err; +@@ -2808,6 +2826,12 @@ av_cold int ff_vaapi_encode_init(AVCodecContext *avctx) + ctx->device = (AVHWDeviceContext*)ctx->device_ref->data; + ctx->hwctx = ctx->device->hwctx; + ++ if (!ctx->hwctx || !ctx->hwctx->funcs) { ++ err = AVERROR(EINVAL); ++ goto fail; ++ } ++ vaf = ctx->hwctx->funcs; ++ + ctx->tail_pkt = av_packet_alloc(); + if (!ctx->tail_pkt) { + err = AVERROR(ENOMEM); +@@ -2864,13 +2888,13 @@ av_cold int ff_vaapi_encode_init(AVCodecContext *avctx) + goto fail; + } + +- vas = vaCreateConfig(ctx->hwctx->display, ++ vas = vaf->vaCreateConfig(ctx->hwctx->display, + ctx->va_profile, ctx->va_entrypoint, + ctx->config_attributes, ctx->nb_config_attributes, + &ctx->va_config); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to create encode pipeline " +- "configuration: %d (%s).\n", vas, vaErrorStr(vas)); ++ "configuration: %d (%s).\n", vas, vaf->vaErrorStr(vas)); + err = AVERROR(EIO); + goto fail; + } +@@ -2880,7 +2904,7 @@ av_cold int ff_vaapi_encode_init(AVCodecContext *avctx) + goto fail; + + recon_hwctx = ctx->recon_frames->hwctx; +- vas = vaCreateContext(ctx->hwctx->display, ctx->va_config, ++ vas = vaf->vaCreateContext(ctx->hwctx->display, ctx->va_config, + ctx->surface_width, ctx->surface_height, + VA_PROGRESSIVE, + recon_hwctx->surface_ids, +@@ -2888,7 +2912,7 @@ av_cold int ff_vaapi_encode_init(AVCodecContext *avctx) + &ctx->va_context); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to create encode pipeline " +- "context: %d (%s).\n", vas, vaErrorStr(vas)); ++ "context: %d (%s).\n", vas, vaf->vaErrorStr(vas)); + err = AVERROR(EIO); + goto fail; + } +@@ -2962,14 +2986,16 @@ av_cold int ff_vaapi_encode_init(AVCodecContext *avctx) + + #if VA_CHECK_VERSION(1, 9, 0) + // check vaSyncBuffer function +- vas = vaSyncBuffer(ctx->hwctx->display, VA_INVALID_ID, 0); +- if (vas != VA_STATUS_ERROR_UNIMPLEMENTED) { +- ctx->has_sync_buffer_func = 1; +- ctx->encode_fifo = av_fifo_alloc2(ctx->async_depth, +- sizeof(VAAPIEncodePicture *), +- 0); +- if (!ctx->encode_fifo) +- return AVERROR(ENOMEM); ++ if (vaf->vaSyncBuffer) { ++ vas = vaf->vaSyncBuffer(ctx->hwctx->display, VA_INVALID_ID, 0); ++ if (vas != VA_STATUS_ERROR_UNIMPLEMENTED) { ++ ctx->has_sync_buffer_func = 1; ++ ctx->encode_fifo = av_fifo_alloc2(ctx->async_depth, ++ sizeof(VAAPIEncodePicture *), ++ 0); ++ if (!ctx->encode_fifo) ++ return AVERROR(ENOMEM); ++ } + } + #endif + +@@ -2997,14 +3023,14 @@ av_cold int ff_vaapi_encode_close(AVCodecContext *avctx) + ff_refstruct_pool_uninit(&ctx->output_buffer_pool); + + if (ctx->va_context != VA_INVALID_ID) { +- if (ctx->hwctx) +- vaDestroyContext(ctx->hwctx->display, ctx->va_context); ++ if (ctx->hwctx && ctx->hwctx->funcs) ++ ctx->hwctx->funcs->vaDestroyContext(ctx->hwctx->display, ctx->va_context); + ctx->va_context = VA_INVALID_ID; + } + + if (ctx->va_config != VA_INVALID_ID) { +- if (ctx->hwctx) +- vaDestroyConfig(ctx->hwctx->display, ctx->va_config); ++ if (ctx->hwctx && ctx->hwctx->funcs) ++ ctx->hwctx->funcs->vaDestroyConfig(ctx->hwctx->display, ctx->va_config); + ctx->va_config = VA_INVALID_ID; + } + +diff --git a/libavcodec/vaapi_encode_av1.c b/libavcodec/vaapi_encode_av1.c +index a46b882ab9..2e64611ab3 100644 +--- a/libavcodec/vaapi_encode_av1.c ++++ b/libavcodec/vaapi_encode_av1.c +@@ -766,6 +766,7 @@ static av_cold int vaapi_encode_av1_init(AVCodecContext *avctx) + { + VAAPIEncodeContext *ctx = avctx->priv_data; + VAAPIEncodeAV1Context *priv = avctx->priv_data; ++ VAAPIDynLoadFunctions *vaf = ctx->hwctx->funcs; + VAConfigAttrib attr; + VAStatus vas; + int ret; +@@ -791,13 +792,13 @@ static av_cold int vaapi_encode_av1_init(AVCodecContext *avctx) + return ret; + + attr.type = VAConfigAttribEncAV1; +- vas = vaGetConfigAttributes(ctx->hwctx->display, ++ vas = vaf->vaGetConfigAttributes(ctx->hwctx->display, + ctx->va_profile, + ctx->va_entrypoint, + &attr, 1); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to query " +- "config attribute: %d (%s).\n", vas, vaErrorStr(vas)); ++ "config attribute: %d (%s).\n", vas, vaf->vaErrorStr(vas)); + return AVERROR_EXTERNAL; + } else if (attr.value == VA_ATTRIB_NOT_SUPPORTED) { + priv->attr.value = 0; +@@ -808,13 +809,13 @@ static av_cold int vaapi_encode_av1_init(AVCodecContext *avctx) + } + + attr.type = VAConfigAttribEncAV1Ext1; +- vas = vaGetConfigAttributes(ctx->hwctx->display, ++ vas = vaf->vaGetConfigAttributes(ctx->hwctx->display, + ctx->va_profile, + ctx->va_entrypoint, + &attr, 1); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to query " +- "config attribute: %d (%s).\n", vas, vaErrorStr(vas)); ++ "config attribute: %d (%s).\n", vas, vaf->vaErrorStr(vas)); + return AVERROR_EXTERNAL; + } else if (attr.value == VA_ATTRIB_NOT_SUPPORTED) { + priv->attr_ext1.value = 0; +@@ -826,13 +827,13 @@ static av_cold int vaapi_encode_av1_init(AVCodecContext *avctx) + + /** This attr provides essential indicators, return error if not support. */ + attr.type = VAConfigAttribEncAV1Ext2; +- vas = vaGetConfigAttributes(ctx->hwctx->display, ++ vas = vaf->vaGetConfigAttributes(ctx->hwctx->display, + ctx->va_profile, + ctx->va_entrypoint, + &attr, 1); + if (vas != VA_STATUS_SUCCESS || attr.value == VA_ATTRIB_NOT_SUPPORTED) { + av_log(avctx, AV_LOG_ERROR, "Failed to query " +- "config attribute: %d (%s).\n", vas, vaErrorStr(vas)); ++ "config attribute: %d (%s).\n", vas, vaf->vaErrorStr(vas)); + return AVERROR_EXTERNAL; + } else { + priv->attr_ext2.value = attr.value; +diff --git a/libavcodec/vaapi_encode_h264.c b/libavcodec/vaapi_encode_h264.c +index 37df9103ae..b83e45d333 100644 +--- a/libavcodec/vaapi_encode_h264.c ++++ b/libavcodec/vaapi_encode_h264.c +@@ -1083,6 +1083,7 @@ static int vaapi_encode_h264_init_slice_params(AVCodecContext *avctx, + static av_cold int vaapi_encode_h264_configure(AVCodecContext *avctx) + { + VAAPIEncodeContext *ctx = avctx->priv_data; ++ VAAPIDynLoadFunctions *vaf = ctx->hwctx->funcs; + VAAPIEncodeH264Context *priv = avctx->priv_data; + int err; + +@@ -1134,7 +1135,7 @@ static av_cold int vaapi_encode_h264_configure(AVCodecContext *avctx) + vaapi_encode_h264_sei_identifier_uuid, + sizeof(priv->sei_identifier.uuid_iso_iec_11578)); + +- driver = vaQueryVendorString(ctx->hwctx->display); ++ driver = vaf->vaQueryVendorString(ctx->hwctx->display); + if (!driver) + driver = "unknown driver"; + +diff --git a/libavcodec/vaapi_encode_h265.c b/libavcodec/vaapi_encode_h265.c +index c4aabbf5ed..9bb85af810 100644 +--- a/libavcodec/vaapi_encode_h265.c ++++ b/libavcodec/vaapi_encode_h265.c +@@ -1199,6 +1199,7 @@ static int vaapi_encode_h265_init_slice_params(AVCodecContext *avctx, + static av_cold int vaapi_encode_h265_get_encoder_caps(AVCodecContext *avctx) + { + VAAPIEncodeContext *ctx = avctx->priv_data; ++ VAAPIDynLoadFunctions *vaf = ctx->hwctx->funcs; + VAAPIEncodeH265Context *priv = avctx->priv_data; + + #if VA_CHECK_VERSION(1, 13, 0) +@@ -1208,7 +1209,7 @@ static av_cold int vaapi_encode_h265_get_encoder_caps(AVCodecContext *avctx) + VAStatus vas; + + attr.type = VAConfigAttribEncHEVCFeatures; +- vas = vaGetConfigAttributes(ctx->hwctx->display, ctx->va_profile, ++ vas = vaf->vaGetConfigAttributes(ctx->hwctx->display, ctx->va_profile, + ctx->va_entrypoint, &attr, 1); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to query encoder " +@@ -1222,7 +1223,7 @@ static av_cold int vaapi_encode_h265_get_encoder_caps(AVCodecContext *avctx) + } + + attr.type = VAConfigAttribEncHEVCBlockSizes; +- vas = vaGetConfigAttributes(ctx->hwctx->display, ctx->va_profile, ++ vas = vaf->vaGetConfigAttributes(ctx->hwctx->display, ctx->va_profile, + ctx->va_entrypoint, &attr, 1); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to query encoder " +diff --git a/libavutil/hwcontext_vaapi.c b/libavutil/hwcontext_vaapi.c +index 95a68e62c5..0e42a36346 100644 +--- a/libavutil/hwcontext_vaapi.c ++++ b/libavutil/hwcontext_vaapi.c +@@ -47,7 +47,7 @@ typedef HRESULT (WINAPI *PFN_CREATE_DXGI_FACTORY)(REFIID riid, void **ppFactory) + #if HAVE_UNISTD_H + # include + #endif +- ++#include + + #include "avassert.h" + #include "buffer.h" +@@ -60,6 +60,129 @@ typedef HRESULT (WINAPI *PFN_CREATE_DXGI_FACTORY)(REFIID riid, void **ppFactory) + #include "pixdesc.h" + #include "pixfmt.h" + ++//////////////////////////////////////////////////////////// ++/// dynamic load functions ++//////////////////////////////////////////////////////////// ++ ++#define LOAD_SYMBOL(name) do { \ ++ funcs->name = dlsym(funcs->handle_va, #name); \ ++ if (!funcs->name) { \ ++ av_log(NULL, AV_LOG_ERROR, "Failed to load %s\n", #name); \ ++ goto fail; \ ++ } \ ++} while(0) ++ ++static void vaapi_free_functions(VAAPIDynLoadFunctions *funcs) ++{ ++ if (!funcs) ++ return; ++ ++ if (funcs->handle_va_x11) ++ dlclose(funcs->handle_va_x11); ++ if (funcs->handle_va_drm) ++ dlclose(funcs->handle_va_drm); ++ if (funcs->handle_va) ++ dlclose(funcs->handle_va); ++ av_free(funcs); ++} ++ ++static VAAPIDynLoadFunctions *vaapi_load_functions(void) ++{ ++ VAAPIDynLoadFunctions *funcs = av_mallocz(sizeof(*funcs)); ++ if (!funcs) ++ return NULL; ++ ++ // Load libva.so ++ funcs->handle_va = dlopen("libva.so.2", RTLD_NOW | RTLD_LOCAL); ++ if (!funcs->handle_va) { ++ av_log(NULL, AV_LOG_ERROR, "Failed to load libva: %s\n", dlerror()); ++ goto fail; ++ } ++ ++ // Load core functions ++ LOAD_SYMBOL(vaInitialize); ++ LOAD_SYMBOL(vaTerminate); ++ LOAD_SYMBOL(vaCreateConfig); ++ LOAD_SYMBOL(vaDestroyConfig); ++ LOAD_SYMBOL(vaCreateContext); ++ LOAD_SYMBOL(vaDestroyContext); ++ LOAD_SYMBOL(vaCreateBuffer); ++ LOAD_SYMBOL(vaDestroyBuffer); ++ LOAD_SYMBOL(vaMapBuffer); ++ LOAD_SYMBOL(vaUnmapBuffer); ++ LOAD_SYMBOL(vaSyncSurface); ++ LOAD_SYMBOL(vaGetConfigAttributes); ++ LOAD_SYMBOL(vaCreateSurfaces); ++ LOAD_SYMBOL(vaDestroySurfaces); ++ LOAD_SYMBOL(vaBeginPicture); ++ LOAD_SYMBOL(vaRenderPicture); ++ LOAD_SYMBOL(vaEndPicture); ++ LOAD_SYMBOL(vaQueryConfigEntrypoints); ++ LOAD_SYMBOL(vaQueryConfigProfiles); ++ LOAD_SYMBOL(vaGetDisplayAttributes); ++ LOAD_SYMBOL(vaErrorStr); ++ LOAD_SYMBOL(vaMaxNumEntrypoints); ++ LOAD_SYMBOL(vaMaxNumProfiles); ++ LOAD_SYMBOL(vaQueryVendorString); ++ LOAD_SYMBOL(vaQuerySurfaceAttributes); ++ LOAD_SYMBOL(vaDestroyImage); ++ LOAD_SYMBOL(vaDeriveImage); ++ LOAD_SYMBOL(vaPutImage); ++ LOAD_SYMBOL(vaCreateImage); ++ LOAD_SYMBOL(vaGetImage); ++ LOAD_SYMBOL(vaExportSurfaceHandle); ++ LOAD_SYMBOL(vaReleaseBufferHandle); ++ LOAD_SYMBOL(vaAcquireBufferHandle); ++ LOAD_SYMBOL(vaSetErrorCallback); ++ LOAD_SYMBOL(vaSetInfoCallback); ++ LOAD_SYMBOL(vaSetDriverName); ++ LOAD_SYMBOL(vaEntrypointStr); ++ LOAD_SYMBOL(vaQueryImageFormats); ++ LOAD_SYMBOL(vaMaxNumImageFormats); ++ LOAD_SYMBOL(vaProfileStr); ++ ++ // Load libva-x11.so ++ funcs->handle_va_x11 = dlopen("libva-x11.so.2", RTLD_NOW | RTLD_LOCAL); ++ if (!funcs->handle_va_x11) { ++ av_log(NULL, AV_LOG_ERROR, "Failed to load libva-x11: %s\n", dlerror()); ++ goto fail; ++ } ++ ++ funcs->vaGetDisplay = dlsym(funcs->handle_va_x11, "vaGetDisplay"); ++ if (!funcs->vaGetDisplay) { ++ av_log(NULL, AV_LOG_ERROR, "Failed to load vaGetDisplay\n"); ++ goto fail; ++ } ++ ++ // Load libva-drm.so ++ funcs->handle_va_drm = dlopen("libva-drm.so.2", RTLD_NOW | RTLD_LOCAL); ++ if (!funcs->handle_va_drm) { ++ av_log(NULL, AV_LOG_ERROR, "Failed to load libva-drm: %s\n", dlerror()); ++ goto fail; ++ } ++ ++ funcs->vaGetDisplayDRM = dlsym(funcs->handle_va_drm, "vaGetDisplayDRM"); ++ if (!funcs->vaGetDisplayDRM) { ++ av_log(NULL, AV_LOG_ERROR, "Failed to load vaGetDisplayDRM\n"); ++ goto fail; ++ } ++ ++ // Optional functions ++ funcs->vaSyncBuffer = dlsym(funcs->handle_va, "vaSyncBuffer"); ++ av_log(NULL, AV_LOG_ERROR, "vaSyncBuffer:%p.\n", funcs->vaSyncBuffer); // use error log level to print it out ++ ++ ++ return funcs; ++ ++fail: ++ vaapi_free_functions(funcs); ++ return NULL; ++} ++ ++//////////////////////////////////////////////////////////// ++/// VAAPI API end ++//////////////////////////////////////////////////////////// ++ + + typedef struct VAAPIDevicePriv { + #if HAVE_VAAPI_X11 +@@ -236,6 +359,7 @@ static int vaapi_frames_get_constraints(AVHWDeviceContext *hwdev, + { + VAAPIDeviceContext *ctx = hwdev->hwctx; + AVVAAPIDeviceContext *hwctx = &ctx->p; ++ VAAPIDynLoadFunctions *vaf = hwctx->funcs; + const AVVAAPIHWConfig *config = hwconfig; + VASurfaceAttrib *attr_list = NULL; + VAStatus vas; +@@ -246,11 +370,11 @@ static int vaapi_frames_get_constraints(AVHWDeviceContext *hwdev, + if (config && + !(hwctx->driver_quirks & AV_VAAPI_DRIVER_QUIRK_SURFACE_ATTRIBUTES)) { + attr_count = 0; +- vas = vaQuerySurfaceAttributes(hwctx->display, config->config_id, ++ vas = vaf->vaQuerySurfaceAttributes(hwctx->display, config->config_id, + 0, &attr_count); + if (vas != VA_STATUS_SUCCESS) { + av_log(hwdev, AV_LOG_ERROR, "Failed to query surface attributes: " +- "%d (%s).\n", vas, vaErrorStr(vas)); ++ "%d (%s).\n", vas, vaf->vaErrorStr(vas)); + err = AVERROR(ENOSYS); + goto fail; + } +@@ -261,11 +385,11 @@ static int vaapi_frames_get_constraints(AVHWDeviceContext *hwdev, + goto fail; + } + +- vas = vaQuerySurfaceAttributes(hwctx->display, config->config_id, ++ vas = vaf->vaQuerySurfaceAttributes(hwctx->display, config->config_id, + attr_list, &attr_count); + if (vas != VA_STATUS_SUCCESS) { + av_log(hwdev, AV_LOG_ERROR, "Failed to query surface attributes: " +- "%d (%s).\n", vas, vaErrorStr(vas)); ++ "%d (%s).\n", vas, vaf->vaErrorStr(vas)); + err = AVERROR(ENOSYS); + goto fail; + } +@@ -396,6 +520,7 @@ static int vaapi_device_init(AVHWDeviceContext *hwdev) + { + VAAPIDeviceContext *ctx = hwdev->hwctx; + AVVAAPIDeviceContext *hwctx = &ctx->p; ++ VAAPIDynLoadFunctions *vaf = hwctx->funcs; + VAImageFormat *image_list = NULL; + VAStatus vas; + const char *vendor_string; +@@ -403,7 +528,7 @@ static int vaapi_device_init(AVHWDeviceContext *hwdev) + enum AVPixelFormat pix_fmt; + unsigned int fourcc; + +- image_count = vaMaxNumImageFormats(hwctx->display); ++ image_count = vaf->vaMaxNumImageFormats(hwctx->display); + if (image_count <= 0) { + err = AVERROR(EIO); + goto fail; +@@ -413,7 +538,7 @@ static int vaapi_device_init(AVHWDeviceContext *hwdev) + err = AVERROR(ENOMEM); + goto fail; + } +- vas = vaQueryImageFormats(hwctx->display, image_list, &image_count); ++ vas = vaf->vaQueryImageFormats(hwctx->display, image_list, &image_count); + if (vas != VA_STATUS_SUCCESS) { + err = AVERROR(EIO); + goto fail; +@@ -440,7 +565,7 @@ static int vaapi_device_init(AVHWDeviceContext *hwdev) + } + } + +- vendor_string = vaQueryVendorString(hwctx->display); ++ vendor_string = vaf->vaQueryVendorString(hwctx->display); + if (vendor_string) + av_log(hwdev, AV_LOG_VERBOSE, "VAAPI driver: %s.\n", vendor_string); + +@@ -493,15 +618,16 @@ static void vaapi_buffer_free(void *opaque, uint8_t *data) + { + AVHWFramesContext *hwfc = opaque; + AVVAAPIDeviceContext *hwctx = hwfc->device_ctx->hwctx; ++ VAAPIDynLoadFunctions *vaf = hwctx->funcs; + VASurfaceID surface_id; + VAStatus vas; + + surface_id = (VASurfaceID)(uintptr_t)data; + +- vas = vaDestroySurfaces(hwctx->display, &surface_id, 1); ++ vas = vaf->vaDestroySurfaces(hwctx->display, &surface_id, 1); + if (vas != VA_STATUS_SUCCESS) { + av_log(hwfc, AV_LOG_ERROR, "Failed to destroy surface %#x: " +- "%d (%s).\n", surface_id, vas, vaErrorStr(vas)); ++ "%d (%s).\n", surface_id, vas, vaf->vaErrorStr(vas)); + } + } + +@@ -511,6 +637,7 @@ static AVBufferRef *vaapi_pool_alloc(void *opaque, size_t size) + VAAPIFramesContext *ctx = hwfc->hwctx; + AVVAAPIFramesContext *avfc = &ctx->p; + AVVAAPIDeviceContext *hwctx = hwfc->device_ctx->hwctx; ++ VAAPIDynLoadFunctions *vaf = hwctx->funcs; + VASurfaceID surface_id; + VAStatus vas; + AVBufferRef *ref; +@@ -519,13 +646,13 @@ static AVBufferRef *vaapi_pool_alloc(void *opaque, size_t size) + avfc->nb_surfaces >= hwfc->initial_pool_size) + return NULL; + +- vas = vaCreateSurfaces(hwctx->display, ctx->rt_format, ++ vas = vaf->vaCreateSurfaces(hwctx->display, ctx->rt_format, + hwfc->width, hwfc->height, + &surface_id, 1, + ctx->attributes, ctx->nb_attributes); + if (vas != VA_STATUS_SUCCESS) { + av_log(hwfc, AV_LOG_ERROR, "Failed to create surface: " +- "%d (%s).\n", vas, vaErrorStr(vas)); ++ "%d (%s).\n", vas, vaf->vaErrorStr(vas)); + return NULL; + } + av_log(hwfc, AV_LOG_DEBUG, "Created surface %#x.\n", surface_id); +@@ -534,7 +661,7 @@ static AVBufferRef *vaapi_pool_alloc(void *opaque, size_t size) + sizeof(surface_id), &vaapi_buffer_free, + hwfc, AV_BUFFER_FLAG_READONLY); + if (!ref) { +- vaDestroySurfaces(hwctx->display, &surface_id, 1); ++ vaf->vaDestroySurfaces(hwctx->display, &surface_id, 1); + return NULL; + } + +@@ -554,6 +681,7 @@ static int vaapi_frames_init(AVHWFramesContext *hwfc) + VAAPIFramesContext *ctx = hwfc->hwctx; + AVVAAPIFramesContext *avfc = &ctx->p; + AVVAAPIDeviceContext *hwctx = hwfc->device_ctx->hwctx; ++ VAAPIDynLoadFunctions *vaf = hwctx->funcs; + const VAAPIFormatDescriptor *desc; + VAImageFormat *expected_format; + AVBufferRef *test_surface = NULL; +@@ -669,7 +797,7 @@ static int vaapi_frames_init(AVHWFramesContext *hwfc) + err = vaapi_get_image_format(hwfc->device_ctx, + hwfc->sw_format, &expected_format); + if (err == 0) { +- vas = vaDeriveImage(hwctx->display, test_surface_id, &test_image); ++ vas = vaf->vaDeriveImage(hwctx->display, test_surface_id, &test_image); + if (vas == VA_STATUS_SUCCESS) { + if (expected_format->fourcc == test_image.format.fourcc) { + av_log(hwfc, AV_LOG_DEBUG, "Direct mapping possible.\n"); +@@ -680,11 +808,11 @@ static int vaapi_frames_init(AVHWFramesContext *hwfc) + "expected format %08x.\n", + expected_format->fourcc, test_image.format.fourcc); + } +- vaDestroyImage(hwctx->display, test_image.image_id); ++ vaf->vaDestroyImage(hwctx->display, test_image.image_id); + } else { + av_log(hwfc, AV_LOG_DEBUG, "Direct mapping disabled: " + "deriving image does not work: " +- "%d (%s).\n", vas, vaErrorStr(vas)); ++ "%d (%s).\n", vas, vaf->vaErrorStr(vas)); + } + } else { + av_log(hwfc, AV_LOG_DEBUG, "Direct mapping disabled: " +@@ -765,33 +893,34 @@ static void vaapi_unmap_frame(AVHWFramesContext *hwfc, + { + AVVAAPIDeviceContext *hwctx = hwfc->device_ctx->hwctx; + VAAPIMapping *map = hwmap->priv; ++ VAAPIDynLoadFunctions *vaf = hwctx->funcs; + VASurfaceID surface_id; + VAStatus vas; + + surface_id = (VASurfaceID)(uintptr_t)hwmap->source->data[3]; + av_log(hwfc, AV_LOG_DEBUG, "Unmap surface %#x.\n", surface_id); + +- vas = vaUnmapBuffer(hwctx->display, map->image.buf); ++ vas = vaf->vaUnmapBuffer(hwctx->display, map->image.buf); + if (vas != VA_STATUS_SUCCESS) { + av_log(hwfc, AV_LOG_ERROR, "Failed to unmap image from surface " +- "%#x: %d (%s).\n", surface_id, vas, vaErrorStr(vas)); ++ "%#x: %d (%s).\n", surface_id, vas, vaf->vaErrorStr(vas)); + } + + if ((map->flags & AV_HWFRAME_MAP_WRITE) && + !(map->flags & AV_HWFRAME_MAP_DIRECT)) { +- vas = vaPutImage(hwctx->display, surface_id, map->image.image_id, ++ vas = vaf->vaPutImage(hwctx->display, surface_id, map->image.image_id, + 0, 0, hwfc->width, hwfc->height, + 0, 0, hwfc->width, hwfc->height); + if (vas != VA_STATUS_SUCCESS) { + av_log(hwfc, AV_LOG_ERROR, "Failed to write image to surface " +- "%#x: %d (%s).\n", surface_id, vas, vaErrorStr(vas)); ++ "%#x: %d (%s).\n", surface_id, vas, vaf->vaErrorStr(vas)); + } + } + +- vas = vaDestroyImage(hwctx->display, map->image.image_id); ++ vas = vaf->vaDestroyImage(hwctx->display, map->image.image_id); + if (vas != VA_STATUS_SUCCESS) { + av_log(hwfc, AV_LOG_ERROR, "Failed to destroy image from surface " +- "%#x: %d (%s).\n", surface_id, vas, vaErrorStr(vas)); ++ "%#x: %d (%s).\n", surface_id, vas, vaf->vaErrorStr(vas)); + } + + av_free(map); +@@ -801,6 +930,7 @@ static int vaapi_map_frame(AVHWFramesContext *hwfc, + AVFrame *dst, const AVFrame *src, int flags) + { + AVVAAPIDeviceContext *hwctx = hwfc->device_ctx->hwctx; ++ VAAPIDynLoadFunctions *vaf = hwctx->funcs; + VAAPIFramesContext *ctx = hwfc->hwctx; + VASurfaceID surface_id; + const VAAPIFormatDescriptor *desc; +@@ -836,10 +966,10 @@ static int vaapi_map_frame(AVHWFramesContext *hwfc, + map->flags = flags; + map->image.image_id = VA_INVALID_ID; + +- vas = vaSyncSurface(hwctx->display, surface_id); ++ vas = vaf->vaSyncSurface(hwctx->display, surface_id); + if (vas != VA_STATUS_SUCCESS) { + av_log(hwfc, AV_LOG_ERROR, "Failed to sync surface " +- "%#x: %d (%s).\n", surface_id, vas, vaErrorStr(vas)); ++ "%#x: %d (%s).\n", surface_id, vas, vaf->vaErrorStr(vas)); + err = AVERROR(EIO); + goto fail; + } +@@ -853,11 +983,11 @@ static int vaapi_map_frame(AVHWFramesContext *hwfc, + // prefer not to be given direct-mapped memory if they request read access. + if (ctx->derive_works && dst->format == hwfc->sw_format && + ((flags & AV_HWFRAME_MAP_DIRECT) || !(flags & AV_HWFRAME_MAP_READ))) { +- vas = vaDeriveImage(hwctx->display, surface_id, &map->image); ++ vas = vaf->vaDeriveImage(hwctx->display, surface_id, &map->image); + if (vas != VA_STATUS_SUCCESS) { + av_log(hwfc, AV_LOG_ERROR, "Failed to derive image from " + "surface %#x: %d (%s).\n", +- surface_id, vas, vaErrorStr(vas)); ++ surface_id, vas, vaf->vaErrorStr(vas)); + err = AVERROR(EIO); + goto fail; + } +@@ -870,32 +1000,32 @@ static int vaapi_map_frame(AVHWFramesContext *hwfc, + } + map->flags |= AV_HWFRAME_MAP_DIRECT; + } else { +- vas = vaCreateImage(hwctx->display, image_format, ++ vas = vaf->vaCreateImage(hwctx->display, image_format, + hwfc->width, hwfc->height, &map->image); + if (vas != VA_STATUS_SUCCESS) { + av_log(hwfc, AV_LOG_ERROR, "Failed to create image for " + "surface %#x: %d (%s).\n", +- surface_id, vas, vaErrorStr(vas)); ++ surface_id, vas, vaf->vaErrorStr(vas)); + err = AVERROR(EIO); + goto fail; + } + if (!(flags & AV_HWFRAME_MAP_OVERWRITE)) { +- vas = vaGetImage(hwctx->display, surface_id, 0, 0, ++ vas = vaf->vaGetImage(hwctx->display, surface_id, 0, 0, + hwfc->width, hwfc->height, map->image.image_id); + if (vas != VA_STATUS_SUCCESS) { + av_log(hwfc, AV_LOG_ERROR, "Failed to read image from " + "surface %#x: %d (%s).\n", +- surface_id, vas, vaErrorStr(vas)); ++ surface_id, vas, vaf->vaErrorStr(vas)); + err = AVERROR(EIO); + goto fail; + } + } + } + +- vas = vaMapBuffer(hwctx->display, map->image.buf, &address); ++ vas = vaf->vaMapBuffer(hwctx->display, map->image.buf, &address); + if (vas != VA_STATUS_SUCCESS) { + av_log(hwfc, AV_LOG_ERROR, "Failed to map image from surface " +- "%#x: %d (%s).\n", surface_id, vas, vaErrorStr(vas)); ++ "%#x: %d (%s).\n", surface_id, vas, vaf->vaErrorStr(vas)); + err = AVERROR(EIO); + goto fail; + } +@@ -924,9 +1054,9 @@ static int vaapi_map_frame(AVHWFramesContext *hwfc, + fail: + if (map) { + if (address) +- vaUnmapBuffer(hwctx->display, map->image.buf); ++ vaf->vaUnmapBuffer(hwctx->display, map->image.buf); + if (map->image.image_id != VA_INVALID_ID) +- vaDestroyImage(hwctx->display, map->image.image_id); ++ vaf->vaDestroyImage(hwctx->display, map->image.image_id); + av_free(map); + } + return err; +@@ -1068,12 +1198,12 @@ static void vaapi_unmap_from_drm(AVHWFramesContext *dst_fc, + HWMapDescriptor *hwmap) + { + AVVAAPIDeviceContext *dst_dev = dst_fc->device_ctx->hwctx; +- ++ VAAPIDynLoadFunctions *vaf = dst_dev->funcs; + VASurfaceID surface_id = (VASurfaceID)(uintptr_t)hwmap->priv; + + av_log(dst_fc, AV_LOG_DEBUG, "Destroy surface %#x.\n", surface_id); + +- vaDestroySurfaces(dst_dev->display, &surface_id, 1); ++ vaf->vaDestroySurfaces(dst_dev->display, &surface_id, 1); + } + + static int vaapi_map_from_drm(AVHWFramesContext *src_fc, AVFrame *dst, +@@ -1088,6 +1218,7 @@ static int vaapi_map_from_drm(AVHWFramesContext *src_fc, AVFrame *dst, + AVHWFramesContext *dst_fc = + (AVHWFramesContext*)dst->hw_frames_ctx->data; + AVVAAPIDeviceContext *dst_dev = dst_fc->device_ctx->hwctx; ++ VAAPIDynLoadFunctions *vaf = dst_dev->funcs; + const AVDRMFrameDescriptor *desc; + const VAAPIFormatDescriptor *format_desc; + VASurfaceID surface_id; +@@ -1204,7 +1335,7 @@ static int vaapi_map_from_drm(AVHWFramesContext *src_fc, AVFrame *dst, + * Gallium seem to do the correct error checks, so lets just try the + * PRIME_2 import first. + */ +- vas = vaCreateSurfaces(dst_dev->display, format_desc->rt_format, ++ vas = vaf->vaCreateSurfaces(dst_dev->display, format_desc->rt_format, + src->width, src->height, &surface_id, 1, + prime_attrs, FF_ARRAY_ELEMS(prime_attrs)); + if (vas != VA_STATUS_SUCCESS) +@@ -1255,7 +1386,7 @@ static int vaapi_map_from_drm(AVHWFramesContext *src_fc, AVFrame *dst, + FFSWAP(uint32_t, buffer_desc.offsets[1], buffer_desc.offsets[2]); + } + +- vas = vaCreateSurfaces(dst_dev->display, format_desc->rt_format, ++ vas = vaf->vaCreateSurfaces(dst_dev->display, format_desc->rt_format, + src->width, src->height, + &surface_id, 1, + buffer_attrs, FF_ARRAY_ELEMS(buffer_attrs)); +@@ -1286,14 +1417,14 @@ static int vaapi_map_from_drm(AVHWFramesContext *src_fc, AVFrame *dst, + FFSWAP(uint32_t, buffer_desc.offsets[1], buffer_desc.offsets[2]); + } + +- vas = vaCreateSurfaces(dst_dev->display, format_desc->rt_format, ++ vas = vaf->vaCreateSurfaces(dst_dev->display, format_desc->rt_format, + src->width, src->height, + &surface_id, 1, + attrs, FF_ARRAY_ELEMS(attrs)); + #endif + if (vas != VA_STATUS_SUCCESS) { + av_log(dst_fc, AV_LOG_ERROR, "Failed to create surface from DRM " +- "object: %d (%s).\n", vas, vaErrorStr(vas)); ++ "object: %d (%s).\n", vas, vaf->vaErrorStr(vas)); + return AVERROR(EIO); + } + av_log(dst_fc, AV_LOG_DEBUG, "Create surface %#x.\n", surface_id); +@@ -1331,6 +1462,7 @@ static int vaapi_map_to_drm_esh(AVHWFramesContext *hwfc, AVFrame *dst, + const AVFrame *src, int flags) + { + AVVAAPIDeviceContext *hwctx = hwfc->device_ctx->hwctx; ++ VAAPIDynLoadFunctions *vaf = hwctx->funcs; + VASurfaceID surface_id; + VAStatus vas; + VADRMPRIMESurfaceDescriptor va_desc; +@@ -1344,10 +1476,10 @@ static int vaapi_map_to_drm_esh(AVHWFramesContext *hwfc, AVFrame *dst, + if (flags & AV_HWFRAME_MAP_READ) { + export_flags |= VA_EXPORT_SURFACE_READ_ONLY; + +- vas = vaSyncSurface(hwctx->display, surface_id); ++ vas = vaf->vaSyncSurface(hwctx->display, surface_id); + if (vas != VA_STATUS_SUCCESS) { + av_log(hwfc, AV_LOG_ERROR, "Failed to sync surface " +- "%#x: %d (%s).\n", surface_id, vas, vaErrorStr(vas)); ++ "%#x: %d (%s).\n", surface_id, vas, vaf->vaErrorStr(vas)); + return AVERROR(EIO); + } + } +@@ -1355,14 +1487,14 @@ static int vaapi_map_to_drm_esh(AVHWFramesContext *hwfc, AVFrame *dst, + if (flags & AV_HWFRAME_MAP_WRITE) + export_flags |= VA_EXPORT_SURFACE_WRITE_ONLY; + +- vas = vaExportSurfaceHandle(hwctx->display, surface_id, ++ vas = vaf->vaExportSurfaceHandle(hwctx->display, surface_id, + VA_SURFACE_ATTRIB_MEM_TYPE_DRM_PRIME_2, + export_flags, &va_desc); + if (vas != VA_STATUS_SUCCESS) { + if (vas == VA_STATUS_ERROR_UNIMPLEMENTED) + return AVERROR(ENOSYS); + av_log(hwfc, AV_LOG_ERROR, "Failed to export surface %#x: " +- "%d (%s).\n", surface_id, vas, vaErrorStr(vas)); ++ "%d (%s).\n", surface_id, vas, vaf->vaErrorStr(vas)); + return AVERROR(EIO); + } + +@@ -1425,6 +1557,7 @@ static void vaapi_unmap_to_drm_abh(AVHWFramesContext *hwfc, + HWMapDescriptor *hwmap) + { + AVVAAPIDeviceContext *hwctx = hwfc->device_ctx->hwctx; ++ VAAPIDynLoadFunctions *vaf = hwctx->funcs; + VAAPIDRMImageBufferMapping *mapping = hwmap->priv; + VASurfaceID surface_id; + VAStatus vas; +@@ -1436,19 +1569,19 @@ static void vaapi_unmap_to_drm_abh(AVHWFramesContext *hwfc, + // DRM PRIME file descriptors are closed by vaReleaseBufferHandle(), + // so we shouldn't close them separately. + +- vas = vaReleaseBufferHandle(hwctx->display, mapping->image.buf); ++ vas = vaf->vaReleaseBufferHandle(hwctx->display, mapping->image.buf); + if (vas != VA_STATUS_SUCCESS) { + av_log(hwfc, AV_LOG_ERROR, "Failed to release buffer " + "handle of image %#x (derived from surface %#x): " + "%d (%s).\n", mapping->image.buf, surface_id, +- vas, vaErrorStr(vas)); ++ vas, vaf->vaErrorStr(vas)); + } + +- vas = vaDestroyImage(hwctx->display, mapping->image.image_id); ++ vas = vaf->vaDestroyImage(hwctx->display, mapping->image.image_id); + if (vas != VA_STATUS_SUCCESS) { + av_log(hwfc, AV_LOG_ERROR, "Failed to destroy image " + "derived from surface %#x: %d (%s).\n", +- surface_id, vas, vaErrorStr(vas)); ++ surface_id, vas, vaf->vaErrorStr(vas)); + } + + av_free(mapping); +@@ -1458,6 +1591,7 @@ static int vaapi_map_to_drm_abh(AVHWFramesContext *hwfc, AVFrame *dst, + const AVFrame *src, int flags) + { + AVVAAPIDeviceContext *hwctx = hwfc->device_ctx->hwctx; ++ VAAPIDynLoadFunctions *vaf = hwctx->funcs; + VAAPIDRMImageBufferMapping *mapping = NULL; + VASurfaceID surface_id; + VAStatus vas; +@@ -1471,12 +1605,12 @@ static int vaapi_map_to_drm_abh(AVHWFramesContext *hwfc, AVFrame *dst, + if (!mapping) + return AVERROR(ENOMEM); + +- vas = vaDeriveImage(hwctx->display, surface_id, ++ vas = vaf->vaDeriveImage(hwctx->display, surface_id, + &mapping->image); + if (vas != VA_STATUS_SUCCESS) { + av_log(hwfc, AV_LOG_ERROR, "Failed to derive image from " + "surface %#x: %d (%s).\n", +- surface_id, vas, vaErrorStr(vas)); ++ surface_id, vas, vaf->vaErrorStr(vas)); + err = AVERROR(EIO); + goto fail; + } +@@ -1531,13 +1665,13 @@ static int vaapi_map_to_drm_abh(AVHWFramesContext *hwfc, AVFrame *dst, + } + } + +- vas = vaAcquireBufferHandle(hwctx->display, mapping->image.buf, ++ vas = vaf->vaAcquireBufferHandle(hwctx->display, mapping->image.buf, + &mapping->buffer_info); + if (vas != VA_STATUS_SUCCESS) { + av_log(hwfc, AV_LOG_ERROR, "Failed to get buffer " + "handle from image %#x (derived from surface %#x): " + "%d (%s).\n", mapping->image.buf, surface_id, +- vas, vaErrorStr(vas)); ++ vas, vaf->vaErrorStr(vas)); + err = AVERROR(EIO); + goto fail_derived; + } +@@ -1566,9 +1700,9 @@ static int vaapi_map_to_drm_abh(AVHWFramesContext *hwfc, AVFrame *dst, + return 0; + + fail_mapped: +- vaReleaseBufferHandle(hwctx->display, mapping->image.buf); ++ vaf->vaReleaseBufferHandle(hwctx->display, mapping->image.buf); + fail_derived: +- vaDestroyImage(hwctx->display, mapping->image.image_id); ++ vaf->vaDestroyImage(hwctx->display, mapping->image.image_id); + fail: + av_freep(&mapping); + return err; +@@ -1622,9 +1756,16 @@ static void vaapi_device_free(AVHWDeviceContext *ctx) + { + AVVAAPIDeviceContext *hwctx = ctx->hwctx; + VAAPIDevicePriv *priv = ctx->user_opaque; ++ VAAPIDynLoadFunctions *vaf = hwctx->funcs; ++ ++ if (hwctx && hwctx->display && vaf && vaf->vaTerminate) ++ vaf->vaTerminate(hwctx->display); + +- if (hwctx->display) +- vaTerminate(hwctx->display); ++ ++ if (hwctx && hwctx->funcs) { ++ vaapi_free_functions(hwctx->funcs); ++ hwctx->funcs = NULL; ++ } + + #if HAVE_VAAPI_X11 + if (priv->x11_display) +@@ -1657,20 +1798,21 @@ static int vaapi_device_connect(AVHWDeviceContext *ctx, + VADisplay display) + { + AVVAAPIDeviceContext *hwctx = ctx->hwctx; ++ VAAPIDynLoadFunctions *vaf = hwctx->funcs; + int major, minor; + VAStatus vas; + + #if CONFIG_VAAPI_1 +- vaSetErrorCallback(display, &vaapi_device_log_error, ctx); +- vaSetInfoCallback (display, &vaapi_device_log_info, ctx); ++ vaf->vaSetErrorCallback(display, &vaapi_device_log_error, ctx); ++ vaf->vaSetInfoCallback (display, &vaapi_device_log_info, ctx); + #endif + + hwctx->display = display; + +- vas = vaInitialize(display, &major, &minor); ++ vas = vaf->vaInitialize(display, &major, &minor); + if (vas != VA_STATUS_SUCCESS) { + av_log(ctx, AV_LOG_ERROR, "Failed to initialise VAAPI " +- "connection: %d (%s).\n", vas, vaErrorStr(vas)); ++ "connection: %d (%s).\n", vas, vaf->vaErrorStr(vas)); + return AVERROR(EIO); + } + av_log(ctx, AV_LOG_VERBOSE, "Initialised VAAPI connection: " +@@ -1686,6 +1828,16 @@ static int vaapi_device_create(AVHWDeviceContext *ctx, const char *device, + VADisplay display = NULL; + const AVDictionaryEntry *ent; + int try_drm, try_x11, try_win32, try_all; ++ VAAPIDeviceContext *hwctx = ctx->hwctx; ++ VAAPIDynLoadFunctions *vaf; ++ ++ hwctx->p.funcs = vaapi_load_functions(); ++ if (!hwctx->p.funcs) { ++ av_log(NULL, AV_LOG_ERROR, "Failed to load libva: %s\n", dlerror()); ++ return AVERROR_EXTERNAL; ++ } ++ ++ vaf = hwctx->p.funcs; + + priv = av_mallocz(sizeof(*priv)); + if (!priv) +@@ -1802,7 +1954,7 @@ static int vaapi_device_create(AVHWDeviceContext *ctx, const char *device, + break; + } + +- display = vaGetDisplayDRM(priv->drm_fd); ++ display = vaf->vaGetDisplayDRM(priv->drm_fd); + if (!display) { + av_log(ctx, AV_LOG_VERBOSE, "Cannot open a VA display " + "from DRM device %s.\n", device); +@@ -1820,7 +1972,7 @@ static int vaapi_device_create(AVHWDeviceContext *ctx, const char *device, + av_log(ctx, AV_LOG_VERBOSE, "Cannot open X11 display " + "%s.\n", XDisplayName(device)); + } else { +- display = vaGetDisplay(priv->x11_display); ++ display = vaf->vaGetDisplay(priv->x11_display); + if (!display) { + av_log(ctx, AV_LOG_ERROR, "Cannot open a VA display " + "from X11 display %s.\n", XDisplayName(device)); +@@ -1909,11 +2061,11 @@ static int vaapi_device_create(AVHWDeviceContext *ctx, const char *device, + if (ent) { + #if VA_CHECK_VERSION(0, 38, 0) + VAStatus vas; +- vas = vaSetDriverName(display, ent->value); ++ vas = vaf->vaSetDriverName(display, ent->value); + if (vas != VA_STATUS_SUCCESS) { + av_log(ctx, AV_LOG_ERROR, "Failed to set driver name to " +- "%s: %d (%s).\n", ent->value, vas, vaErrorStr(vas)); +- vaTerminate(display); ++ "%s: %d (%s).\n", ent->value, vas, vaf->vaErrorStr(vas)); ++ vaf->vaTerminate(display); + return AVERROR_EXTERNAL; + } + #else +@@ -1929,6 +2081,8 @@ static int vaapi_device_derive(AVHWDeviceContext *ctx, + AVHWDeviceContext *src_ctx, + AVDictionary *opts, int flags) + { ++ VAAPIDeviceContext *hwctx = ctx->hwctx; ++ VAAPIDynLoadFunctions *vaf = hwctx->p.funcs; + #if HAVE_VAAPI_DRM + if (src_ctx->type == AV_HWDEVICE_TYPE_DRM) { + AVDRMDeviceContext *src_hwctx = src_ctx->hwctx; +@@ -2000,7 +2154,7 @@ static int vaapi_device_derive(AVHWDeviceContext *ctx, + ctx->user_opaque = priv; + ctx->free = &vaapi_device_free; + +- display = vaGetDisplayDRM(fd); ++ display = vaf->vaGetDisplayDRM(fd); + if (!display) { + av_log(ctx, AV_LOG_ERROR, "Failed to open a VA display from " + "DRM device.\n"); +@@ -2010,6 +2164,7 @@ static int vaapi_device_derive(AVHWDeviceContext *ctx, + return vaapi_device_connect(ctx, display); + } + #endif ++ + return AVERROR(ENOSYS); + } + +@@ -2040,3 +2195,4 @@ const HWContextType ff_hwcontext_type_vaapi = { + AV_PIX_FMT_NONE + }, + }; ++ +diff --git a/libavutil/hwcontext_vaapi.h b/libavutil/hwcontext_vaapi.h +index 0b2e071cb3..7bdb21c66a 100644 +--- a/libavutil/hwcontext_vaapi.h ++++ b/libavutil/hwcontext_vaapi.h +@@ -20,6 +20,100 @@ + #define AVUTIL_HWCONTEXT_VAAPI_H + + #include ++#include ++#include ++#include ++ ++ ++//////////////////////////////////////////////////////////// ++/// VAAPI dynamic load functions start ++//////////////////////////////////////////////////////////// ++ ++typedef struct VAAPIDynLoadFunctions { ++ // Core VA functions ++ VAStatus (*vaInitialize)(VADisplay dpy, int *major_version, int *minor_version); ++ VAStatus (*vaTerminate)(VADisplay dpy); ++ VAStatus (*vaCreateConfig)(VADisplay dpy, VAProfile profile, VAEntrypoint entrypoint, ++ VAConfigAttrib *attrib_list, int num_attribs, VAConfigID *config_id); ++ VAStatus (*vaDestroyConfig)(VADisplay dpy, VAConfigID config_id); ++ VAStatus (*vaCreateContext)(VADisplay dpy, VAConfigID config_id, int picture_width, ++ int picture_height, int flag, VASurfaceID *render_targets, ++ int num_render_targets, VAContextID *context); ++ VAStatus (*vaDestroyContext)(VADisplay dpy, VAContextID context); ++ VAStatus (*vaCreateBuffer)(VADisplay dpy, VAContextID context, VABufferType type, ++ unsigned int size, unsigned int num_elements, void *data, ++ VABufferID *buf_id); ++ VAStatus (*vaDestroyBuffer)(VADisplay dpy, VABufferID buf_id); ++ VAStatus (*vaMapBuffer)(VADisplay dpy, VABufferID buf_id, void **pbuf); ++ VAStatus (*vaUnmapBuffer)(VADisplay dpy, VABufferID buf_id); ++ VAStatus (*vaSyncSurface)(VADisplay dpy, VASurfaceID render_target); ++ VAStatus (*vaGetConfigAttributes)(VADisplay dpy, VAProfile profile, ++ VAEntrypoint entrypoint, VAConfigAttrib *attrib_list, ++ int num_attribs); ++ VAStatus (*vaCreateSurfaces)(VADisplay dpy, unsigned int format, ++ unsigned int width, unsigned int height, ++ VASurfaceID *surfaces, unsigned int num_surfaces, ++ VASurfaceAttrib *attrib_list, unsigned int num_attribs); ++ VAStatus (*vaDestroySurfaces)(VADisplay dpy, VASurfaceID *surfaces, int num_surfaces); ++ VAStatus (*vaBeginPicture)(VADisplay dpy, VAContextID context, VASurfaceID render_target); ++ VAStatus (*vaRenderPicture)(VADisplay dpy, VAContextID context, ++ VABufferID *buffers, int num_buffers); ++ VAStatus (*vaEndPicture)(VADisplay dpy, VAContextID context); ++ VAStatus (*vaQueryConfigEntrypoints)(VADisplay dpy, VAProfile profile, ++ VAEntrypoint *entrypoint_list, int *num_entrypoints); ++ VAStatus (*vaQueryConfigProfiles)(VADisplay dpy, VAProfile *profile_list, int *num_profiles); ++ VAStatus (*vaGetDisplayAttributes)(VADisplay dpy, VADisplayAttribute *attr_list, int num_attributes); ++ const char *(*vaErrorStr)(VAStatus error_status); ++ int (*vaMaxNumEntrypoints)(VADisplay dpy); ++ int (*vaMaxNumProfiles)(VADisplay dpy); ++ const char *(*vaQueryVendorString)(VADisplay dpy); ++ VAStatus (*vaQuerySurfaceAttributes)(VADisplay dpy, VAConfigID config_id, ++ VASurfaceAttrib *attrib_list, int *num_attribs); ++ VAStatus (*vaDestroyImage)(VADisplay dpy, VAImageID image); ++ VAStatus (*vaDeriveImage)(VADisplay dpy, VASurfaceID surface, VAImage *image); ++ VAStatus (*vaPutImage)(VADisplay dpy, VASurfaceID surface, VAImageID image, ++ int src_x, int src_y, unsigned int src_width, unsigned int src_height, ++ int dest_x, int dest_y, unsigned int dest_width, unsigned int dest_height); ++ VAStatus (*vaCreateImage)(VADisplay dpy, VAImageFormat *format, int width, int height, VAImage *image); ++ VAStatus (*vaGetImage)(VADisplay dpy, VASurfaceID surface, ++ int x, int y, unsigned int width, unsigned int height, ++ VAImageID image); ++ VAStatus (*vaExportSurfaceHandle)(VADisplay dpy, VASurfaceID surface_id, ++ uint32_t mem_type, uint32_t flags, ++ void *descriptor); ++ VAStatus (*vaReleaseBufferHandle)(VADisplay dpy, VABufferID buf_id); ++ VAStatus (*vaAcquireBufferHandle)(VADisplay dpy, VABufferID buf_id, ++ VABufferInfo *buf_info); ++ VAStatus (*vaSetErrorCallback)(VADisplay dpy, VAMessageCallback callback, void *user_context); ++ VAStatus (*vaSetInfoCallback)(VADisplay dpy, VAMessageCallback callback, void *user_context); ++ VAStatus (*vaSetDriverName)(VADisplay dpy, const char *driver_name); ++ const char *(*vaEntrypointStr)(VAEntrypoint entrypoint); ++ VAStatus (*vaQueryImageFormats)(VADisplay dpy, VAImageFormat *format_list, int *num_formats); ++ int (*vaMaxNumImageFormats)(VADisplay dpy); ++ const char *(*vaProfileStr)(VAProfile profile); ++ ++ ++ // Optional functions ++ VAStatus (*vaSyncBuffer)(VADisplay dpy, VABufferID buf_id, uint64_t timeout_ns); ++ ++ // X11 specific functions ++ VADisplay (*vaGetDisplay)(Display *dpy); ++ ++ // DRM specific functions ++ VADisplay (*vaGetDisplayDRM)(int fd); ++ ++ ++ ++ // Library handles ++ void *handle_va; ++ void *handle_va_drm; ++ void *handle_va_x11; ++} VAAPIDynLoadFunctions; ++ ++ ++//////////////////////////////////////////////////////////// ++/// VAAPI API end ++//////////////////////////////////////////////////////////// + + /** + * @file +@@ -78,6 +172,8 @@ typedef struct AVVAAPIDeviceContext { + * operations using VAAPI with the same VADisplay. + */ + unsigned int driver_quirks; ++ ++ VAAPIDynLoadFunctions *funcs; + } AVVAAPIDeviceContext; + + /** +@@ -114,4 +210,5 @@ typedef struct AVVAAPIHWConfig { + VAConfigID config_id; + } AVVAAPIHWConfig; + ++ + #endif /* AVUTIL_HWCONTEXT_VAAPI_H */ +diff --git a/libavutil/hwcontext_vulkan.c b/libavutil/hwcontext_vulkan.c +index 6e3b96b73a..55ba57ea7d 100644 +--- a/libavutil/hwcontext_vulkan.c ++++ b/libavutil/hwcontext_vulkan.c +@@ -1597,6 +1597,7 @@ static int vulkan_device_derive(AVHWDeviceContext *ctx, + #if CONFIG_VAAPI + case AV_HWDEVICE_TYPE_VAAPI: { + AVVAAPIDeviceContext *src_hwctx = src_ctx->hwctx; ++ VAAPIDynLoadFunctions *vaf = src_hwctx->funcs; + VADisplay dpy = src_hwctx->display; + #if VA_CHECK_VERSION(1, 15, 0) + VAStatus vas; +@@ -1607,13 +1608,13 @@ static int vulkan_device_derive(AVHWDeviceContext *ctx, + const char *vendor; + + #if VA_CHECK_VERSION(1, 15, 0) +- vas = vaGetDisplayAttributes(dpy, &attr, 1); ++ vas = vaf->vaGetDisplayAttributes(dpy, &attr, 1); + if (vas == VA_STATUS_SUCCESS && attr.flags != VA_DISPLAY_ATTRIB_NOT_SUPPORTED) + dev_select.pci_device = (attr.value & 0xFFFF); + #endif + + if (!dev_select.pci_device) { +- vendor = vaQueryVendorString(dpy); ++ vendor = vaf->vaQueryVendorString(dpy); + if (!vendor) { + av_log(ctx, AV_LOG_ERROR, "Unable to get device info from VAAPI!\n"); + return AVERROR_EXTERNAL; +-- +2.34.1 + diff --git a/res/vcpkg/ffmpeg/portfile.cmake b/res/vcpkg/ffmpeg/portfile.cmake index d56475c059f8..0e35a9550c04 100644 --- a/res/vcpkg/ffmpeg/portfile.cmake +++ b/res/vcpkg/ffmpeg/portfile.cmake @@ -15,6 +15,7 @@ vcpkg_from_github( patch/0003-amf-colorspace.patch patch/0004-videotoolbox-changing-bitrate.patch patch/0005-mediacodec-changing-bitrate.patch + patch/0006-dlopen-libva.patch ) if(SOURCE_PATH MATCHES " ") @@ -79,13 +80,15 @@ else() endif() if(VCPKG_TARGET_IS_LINUX) - string(APPEND OPTIONS "\ + string(APPEND OPTIONS "\ --target-os=linux \ --enable-pthreads \ +--disable-vdpau \ ") + if(VCPKG_TARGET_ARCHITECTURE STREQUAL "arm") else() - string(APPEND OPTIONS "\ + string(APPEND OPTIONS "\ --enable-cuda \ --enable-ffnvcodec \ --enable-encoder=h264_nvenc \ @@ -100,8 +103,9 @@ if(VCPKG_TARGET_IS_LINUX) --enable-encoder=h264_vaapi \ --enable-encoder=hevc_vaapi \ ") + if(VCPKG_TARGET_ARCHITECTURE STREQUAL "x64") - string(APPEND OPTIONS "\ + string(APPEND OPTIONS "\ --enable-cuda_llvm \ ") endif() @@ -129,7 +133,8 @@ elseif(VCPKG_TARGET_IS_WINDOWS) --enable-libmfx \ --enable-encoder=h264_qsv \ --enable-encoder=hevc_qsv \ -") +") + if(VCPKG_TARGET_ARCHITECTURE STREQUAL "x86") set(LIB_MACHINE_ARG /machine:x86) string(APPEND OPTIONS " --arch=i686 --enable-cross-compile") @@ -191,6 +196,7 @@ endif() string(APPEND VCPKG_COMBINED_C_FLAGS_DEBUG " -I \"${CURRENT_INSTALLED_DIR}/include\"") string(APPEND VCPKG_COMBINED_C_FLAGS_RELEASE " -I \"${CURRENT_INSTALLED_DIR}/include\"") + if(VCPKG_TARGET_IS_WINDOWS) string(APPEND VCPKG_COMBINED_C_FLAGS_DEBUG " -I \"${CURRENT_INSTALLED_DIR}/include/mfx\"") string(APPEND VCPKG_COMBINED_C_FLAGS_RELEASE " -I \"${CURRENT_INSTALLED_DIR}/include/mfx\"") @@ -204,9 +210,11 @@ if(VCPKG_DETECTED_CMAKE_C_COMPILER) get_filename_component(CC_filename "${VCPKG_DETECTED_CMAKE_C_COMPILER}" NAME) set(ENV{CC} "${CC_filename}") string(APPEND OPTIONS " --cc=${CC_filename}") + if(VCPKG_HOST_IS_WINDOWS) string(APPEND OPTIONS " --host_cc=${CC_filename}") endif() + list(APPEND prog_env "${CC_path}") endif() @@ -284,6 +292,7 @@ if(VCPKG_HOST_IS_WINDOWS) else() # find_program(SHELL bash) endif() + list(REMOVE_DUPLICATES prog_env) vcpkg_add_to_path(PREPEND ${prog_env}) From a23822074ec5199c9fd462ffe5c9b1308601739c Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Wed, 4 Dec 2024 17:10:32 +0800 Subject: [PATCH 447/541] feat: Android, opt, check update on startup (#10165) * feat: Android, opt, check update on startup Signed-off-by: fufesou * refact: check update only on startup Signed-off-by: fufesou * fix: Android, "Download new version" Signed-off-by: fufesou --------- Signed-off-by: fufesou --- .../android/app/src/main/AndroidManifest.xml | 7 ++++ flutter/lib/common.dart | 17 +++++++++ .../lib/desktop/pages/desktop_home_page.dart | 21 +++-------- .../desktop/pages/desktop_setting_page.dart | 1 + flutter/lib/main.dart | 2 ++ flutter/lib/mobile/pages/connection_page.dart | 36 ++++++++----------- flutter/lib/mobile/pages/settings_page.dart | 25 +++++++++++++ flutter/lib/models/state_model.dart | 2 ++ src/flutter_ffi.rs | 3 +- 9 files changed, 74 insertions(+), 40 deletions(-) diff --git a/flutter/android/app/src/main/AndroidManifest.xml b/flutter/android/app/src/main/AndroidManifest.xml index 51015f74a831..47533612b3d5 100644 --- a/flutter/android/app/src/main/AndroidManifest.xml +++ b/flutter/android/app/src/main/AndroidManifest.xml @@ -15,6 +15,13 @@ + + + + + + + ? get subWindowManagerEnableResizeEdges => isWindows void earlyAssert() { assert('\1' == '1'); } + +void checkUpdate() { + if (isDesktop || isAndroid) { + if (!bind.isCustomClient()) { + platformFFI.registerEventHandler( + kCheckSoftwareUpdateFinish, kCheckSoftwareUpdateFinish, + (Map evt) async { + if (evt['url'] is String) { + stateGlobal.updateUrl.value = evt['url']; + } + }); + Timer(const Duration(seconds: 1), () async { + bind.mainGetSoftwareUpdateUrl(); + }); + } + } +} diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index 04a186b84c63..754efbd5ae83 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -14,6 +14,7 @@ import 'package:flutter_hbb/desktop/pages/desktop_setting_page.dart'; import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart'; import 'package:flutter_hbb/models/platform_model.dart'; import 'package:flutter_hbb/models/server_model.dart'; +import 'package:flutter_hbb/models/state_model.dart'; import 'package:flutter_hbb/plugin/ui_manager.dart'; import 'package:flutter_hbb/utils/multi_window_manager.dart'; import 'package:get/get.dart'; @@ -39,7 +40,6 @@ class _DesktopHomePageState extends State @override bool get wantKeepAlive => true; - var updateUrl = ''; var systemError = ''; StreamSubscription? _uniLinksSubscription; var svcStopped = false.obs; @@ -86,7 +86,8 @@ class _DesktopHomePageState extends State if (!isOutgoingOnly) buildIDBoard(context), if (!isOutgoingOnly) buildPasswordBoard(context), FutureBuilder( - future: buildHelpCards(), + future: Future.value( + Obx(() => buildHelpCards(stateGlobal.updateUrl.value))), builder: (_, data) { if (data.hasData) { if (isIncomingOnly) { @@ -415,7 +416,7 @@ class _DesktopHomePageState extends State ); } - Future buildHelpCards() async { + Widget buildHelpCards(String updateUrl) { if (!bind.isCustomClient() && updateUrl.isNotEmpty && !isCardClosed && @@ -669,20 +670,6 @@ class _DesktopHomePageState extends State @override void initState() { super.initState(); - if (!bind.isCustomClient()) { - platformFFI.registerEventHandler( - kCheckSoftwareUpdateFinish, kCheckSoftwareUpdateFinish, - (Map 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 { await gFFI.serverModel.fetchID(); final error = await bind.mainGetError(); diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 6f220a35f7b2..44cdbcbb81b2 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -14,6 +14,7 @@ import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart'; import 'package:flutter_hbb/mobile/widgets/dialog.dart'; import 'package:flutter_hbb/models/platform_model.dart'; import 'package:flutter_hbb/models/server_model.dart'; +import 'package:flutter_hbb/models/state_model.dart'; import 'package:flutter_hbb/plugin/manager.dart'; import 'package:flutter_hbb/plugin/widgets/desktop_settings.dart'; import 'package:get/get.dart'; diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index 3176bfb86eba..301a9f25ce4c 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -120,6 +120,7 @@ Future initEnv(String appType) async { void runMainApp(bool startService) async { // register uni links await initEnv(kAppTypeMain); + checkUpdate(); // trigger connection status updater await bind.mainCheckConnectStatus(); if (startService) { @@ -156,6 +157,7 @@ void runMainApp(bool startService) async { void runMobileApp() async { await initEnv(kAppTypeMain); + checkUpdate(); if (isAndroid) androidChannelInit(); if (isAndroid) platformFFI.syncAndroidServiceAppDirConfigPath(); draggablePositions.load(); diff --git a/flutter/lib/mobile/pages/connection_page.dart b/flutter/lib/mobile/pages/connection_page.dart index de68aa510b67..49e3b2c91077 100644 --- a/flutter/lib/mobile/pages/connection_page.dart +++ b/flutter/lib/mobile/pages/connection_page.dart @@ -4,6 +4,7 @@ import 'package:auto_size_text_field/auto_size_text_field.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/common/formatter/id_formatter.dart'; import 'package:flutter_hbb/common/widgets/connection_page_title.dart'; +import 'package:flutter_hbb/models/state_model.dart'; import 'package:get/get.dart'; import 'package:provider/provider.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -40,8 +41,6 @@ class _ConnectionPageState extends State { final _idController = IDTextEditingController(); final RxBool _idEmpty = true.obs; - /// Update url. If it's not null, means an update is available. - var _updateUrl = ''; List peers = []; bool isPeersLoading = false; @@ -72,22 +71,6 @@ class _ConnectionPageState extends State { } }); } - if (isAndroid) { - if (!bind.isCustomClient()) { - platformFFI.registerEventHandler( - kCheckSoftwareUpdateFinish, kCheckSoftwareUpdateFinish, - (Map evt) async { - if (evt['url'] is String) { - setState(() { - _updateUrl = evt['url']; - }); - } - }); - Timer(const Duration(seconds: 1), () async { - bind.mainGetSoftwareUpdateUrl(); - }); - } - } } @override @@ -97,7 +80,8 @@ class _ConnectionPageState extends State { slivers: [ SliverList( delegate: SliverChildListDelegate([ - if (!bind.isCustomClient()) _buildUpdateUI(), + if (!bind.isCustomClient()) + Obx(() => _buildUpdateUI(stateGlobal.updateUrl.value)), _buildRemoteIDTextField(), ])), SliverFillRemaining( @@ -116,13 +100,21 @@ class _ConnectionPageState extends State { } /// UI for software update. - /// If [_updateUrl] is not empty, shows a button to update the software. - Widget _buildUpdateUI() { - return _updateUrl.isEmpty + /// If _updateUrl] is not empty, shows a button to update the software. + Widget _buildUpdateUI(String updateUrl) { + return updateUrl.isEmpty ? const SizedBox(height: 0) : InkWell( onTap: () async { final url = 'https://rustdesk.com/download'; + // https://pub.dev/packages/url_launcher#configuration + // https://developer.android.com/training/package-visibility/use-cases#open-urls-custom-tabs + // + // `await launchUrl(Uri.parse(url))` can also run if skip + // 1. The following check + // 2. `` in AndroidManifest.xml + // + // But it is better to add the check. if (await canLaunchUrl(Uri.parse(url))) { await launchUrl(Uri.parse(url)); } diff --git a/flutter/lib/mobile/pages/settings_page.dart b/flutter/lib/mobile/pages/settings_page.dart index 9e265810fc98..ede66d78ae7c 100644 --- a/flutter/lib/mobile/pages/settings_page.dart +++ b/flutter/lib/mobile/pages/settings_page.dart @@ -5,6 +5,7 @@ import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/common/widgets/setting_widgets.dart'; import 'package:flutter_hbb/desktop/pages/desktop_setting_page.dart'; +import 'package:flutter_hbb/models/state_model.dart'; import 'package:get/get.dart'; import 'package:provider/provider.dart'; import 'package:settings_ui/settings_ui.dart'; @@ -70,6 +71,7 @@ class _SettingsState extends State with WidgetsBindingObserver { false; //androidVersion >= 26; // remove because not work on every device var _ignoreBatteryOpt = false; var _enableStartOnBoot = false; + var _checkUpdateOnStartup = false; var _floatingWindowDisabled = false; var _keepScreenOn = KeepScreenOn.duringControlled; // relay on floating window var _enableAbr = false; @@ -154,6 +156,13 @@ class _SettingsState extends State with WidgetsBindingObserver { _enableStartOnBoot = enableStartOnBoot; } + var checkUpdateOnStartup = + mainGetLocalBoolOptionSync(kOptionEnableCheckUpdate); + if (checkUpdateOnStartup != _checkUpdateOnStartup) { + update = true; + _checkUpdateOnStartup = checkUpdateOnStartup; + } + var floatingWindowDisabled = bind.mainGetLocalOption(key: kOptionDisableFloatingWindow) == "Y" || !await AndroidPermissionManager.check(kSystemAlertWindow); @@ -552,6 +561,22 @@ class _SettingsState extends State with WidgetsBindingObserver { gFFI.invokeMethod(AndroidChannel.kSetStartOnBootOpt, toValue); })); + if (!bind.isCustomClient()) { + enhancementsTiles.add( + SettingsTile.switchTile( + initialValue: _checkUpdateOnStartup, + title: + Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Text(translate('Check for software update on startup')), + ]), + onToggle: (bool toValue) async { + await mainSetLocalBoolOption(kOptionEnableCheckUpdate, toValue); + setState(() => _checkUpdateOnStartup = toValue); + }, + ), + ); + } + onFloatingWindowChanged(bool toValue) async { if (toValue) { if (!await AndroidPermissionManager.check(kSystemAlertWindow)) { diff --git a/flutter/lib/models/state_model.dart b/flutter/lib/models/state_model.dart index f8f06cc3fb8a..f096036498e9 100644 --- a/flutter/lib/models/state_model.dart +++ b/flutter/lib/models/state_model.dart @@ -25,6 +25,8 @@ class StateGlobal { final isPortrait = false.obs; + final updateUrl = ''.obs; + String _inputSource = ''; // Use for desktop -> remote toolbar -> resolution diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 4630ac3337d7..5b6c48b95be2 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -1418,7 +1418,8 @@ pub fn main_get_last_remote_id() -> String { } pub fn main_get_software_update_url() { - if get_local_option("enable-check-update".to_string()) != "N" { + let opt = get_local_option(config::keys::OPTION_ENABLE_CHECK_UPDATE.to_string()); + if config::option2bool(config::keys::OPTION_ENABLE_CHECK_UPDATE, &opt) { crate::common::check_software_update(); } } From f13ef48cec91ec7034ba905a6978b9e48b91e1b1 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Thu, 5 Dec 2024 10:38:22 +0800 Subject: [PATCH 448/541] fix: macos, aarch64, try fix running on 12.3 (#10183) Signed-off-by: fufesou --- Cargo.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 7d0b26d71e01..b4ee9dd2d4b6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1290,7 +1290,7 @@ dependencies = [ [[package]] name = "cpal" version = "0.15.3" -source = "git+https://github.com/rustdesk-org/cpal?branch=osx-screencapturekit#4d318ff778063ce14669fd4bd67a1673653fc6e5" +source = "git+https://github.com/rustdesk-org/cpal?branch=osx-screencapturekit#7cb4ed0bd5546bf209edde0d0e9da5194753f2c0" dependencies = [ "alsa", "cidre", From fe4094777f14fe5835c46ec892818efe7eb1483c Mon Sep 17 00:00:00 2001 From: RustDesk <71636191+rustdesk@users.noreply.github.com> Date: Thu, 5 Dec 2024 11:44:37 +0900 Subject: [PATCH 449/541] Revert "fix: linux, window, workaround, mint, mate (#10146)" (#10184) This reverts commit bd0a33e46796a9f24bba6fabb2534490fe1757a0. --- flutter/lib/common.dart | 30 -------- flutter/lib/main.dart | 11 +-- flutter/lib/web/bridge.dart | 4 -- flutter/linux/my_application.cc | 98 ++++----------------------- libs/hbb_common/src/platform/linux.rs | 15 +--- src/flutter_ffi.rs | 20 ------ 6 files changed, 17 insertions(+), 161 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 0eaeb1bae10d..0dd8849f363e 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -1,7 +1,6 @@ import 'dart:async'; import 'dart:convert'; import 'dart:math'; -import 'dart:io'; import 'package:back_button_interceptor/back_button_interceptor.dart'; import 'package:desktop_multi_window/desktop_multi_window.dart'; @@ -3460,35 +3459,6 @@ Widget buildPresetPasswordWarning() { ); } -bool get isLinuxMateDesktop => - isLinux && - (Platform.environment['XDG_CURRENT_DESKTOP']?.toLowerCase() == 'mate' || - Platform.environment['XDG_SESSION_DESKTOP']?.toLowerCase() == 'mate' || - Platform.environment['DESKTOP_SESSION']?.toLowerCase() == 'mate'); - -Map? _linuxOsDistro; - -String getLinuxOsDistroId() { - if (_linuxOsDistro == null) { - String osInfo = bind.getOsDistroInfo(); - if (osInfo.isEmpty) { - _linuxOsDistro = {}; - } else { - try { - _linuxOsDistro = jsonDecode(osInfo); - } catch (e) { - debugPrint('Failed to parse os info: $e'); - // Don't call `bind.getOsDistroInfo()` again if failed to parse osInfo. - _linuxOsDistro = {}; - } - } - } - return (_linuxOsDistro?['id'] ?? '') as String; -} - -bool get isLinuxMint => - getLinuxOsDistroId().toLowerCase().contains('linuxmint'); - // https://github.com/leanflutter/window_manager/blob/87dd7a50b4cb47a375b9fc697f05e56eea0a2ab3/lib/src/widgets/virtual_window_frame.dart#L44 Widget buildVirtualWindowFrame(BuildContext context, Widget child) { boxShadow() => isMainDesktopWindow diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index 301a9f25ce4c..3032a2321f00 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -485,16 +485,7 @@ class _AppState extends State with WidgetsBindingObserver { child = keyListenerBuilder(context, child); } if (isLinux) { - // `(!(isLinuxMateDesktop || isLinuxMint))` is not used here for clarity. - // `isLinuxMint` will call ffi function. - if (!isLinuxMateDesktop) { - if (!isLinuxMint) { - debugPrint( - 'Linux distro is not linuxmint, and desktop is not mate, ' - 'so we build virtual window frame.'); - child = buildVirtualWindowFrame(context, child); - } - } + child = buildVirtualWindowFrame(context, child); } return child; }, diff --git a/flutter/lib/web/bridge.dart b/flutter/lib/web/bridge.dart index 498a464aee3e..dba7fc0941c8 100644 --- a/flutter/lib/web/bridge.dart +++ b/flutter/lib/web/bridge.dart @@ -1848,9 +1848,5 @@ class RustdeskImpl { throw UnimplementedError("sessionGetConnToken"); } - String getOsDistroInfo({dynamic hint}) { - return ''; - } - void dispose() {} } diff --git a/flutter/linux/my_application.cc b/flutter/linux/my_application.cc index 9fa947002d3e..f4247bd94613 100644 --- a/flutter/linux/my_application.cc +++ b/flutter/linux/my_application.cc @@ -17,7 +17,6 @@ G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) extern bool gIsConnectionManager; GtkWidget *find_gl_area(GtkWidget *widget); -void try_set_transparent(GtkWindow* window, GdkScreen* screen, FlView* view); // Implements GApplication::activate. static void my_application_activate(GApplication* application) { @@ -80,7 +79,21 @@ static void my_application_activate(GApplication* application) { gtk_widget_show(GTK_WIDGET(view)); gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); - try_set_transparent(window, screen, view); + // https://github.com/flutter/flutter/issues/152154 + // Remove this workaround when flutter version is updated. + GtkWidget *gl_area = find_gl_area(GTK_WIDGET(view)); + if (gl_area != NULL) { + gtk_gl_area_set_has_alpha(GTK_GL_AREA(gl_area), TRUE); + } + + if (screen != NULL) { + GdkVisual *visual = NULL; + gtk_widget_set_app_paintable(GTK_WIDGET(window), TRUE); + visual = gdk_screen_get_rgba_visual(screen); + if (visual != NULL && gdk_screen_is_composited(screen)) { + gtk_widget_set_visual(GTK_WIDGET(window), visual); + } + } fl_register_plugins(FL_PLUGIN_REGISTRY(view)); @@ -149,84 +162,3 @@ GtkWidget *find_gl_area(GtkWidget *widget) return NULL; } - -bool is_linux_mint() -{ - bool is_mint = false; - char line[256]; - FILE *fp = fopen("/etc/os-release", "r"); - if (fp == NULL) { - return false; - } - while (fgets(line, sizeof(line), fp)) { - if (strstr(line, "ID=linuxmint") != NULL) { - is_mint = true; - break; - } - } - fclose(fp); - - return is_mint; -} - -bool is_desktop_mate() -{ - const char* desktop = NULL; - desktop = getenv("XDG_CURRENT_DESKTOP"); - printf("Linux desktop, XDG_CURRENT_DESKTOP: %s\n", desktop == NULL ? "" : desktop); - if (desktop == NULL) { - desktop = getenv("XDG_SESSION_DESKTOP"); - printf("Linux desktop, XDG_SESSION_DESKTOP: %s\n", desktop == NULL ? "" : desktop); - } - if (desktop == NULL) { - desktop = getenv("DESKTOP_SESSION"); - printf("Linux desktop, DESKTOP_SESSION: %s\n", desktop == NULL ? "" : desktop); - } - if (desktop != NULL && strcasecmp(desktop, "mate") == 0) { - return true; - } - return false; -} - -bool skip_setting_transparent() -{ - if (is_desktop_mate()) { - printf("Linux desktop, MATE\n"); - return true; - } - - if (is_linux_mint()) { - printf("Linux desktop, Linux Mint\n"); - return true; - } - - return false; -} - -// https://github.com/flutter/flutter/issues/152154 -// Remove this workaround when flutter version is updated. -void try_set_transparent(GtkWindow* window, GdkScreen* screen, FlView* view) -{ - GtkWidget *gl_area = NULL; - - if (skip_setting_transparent()) { - printf("Skip setting transparent\n"); - return; - } - - printf("Try setting transparent\n"); - - gl_area = find_gl_area(GTK_WIDGET(view)); - if (gl_area != NULL) { - gtk_gl_area_set_has_alpha(GTK_GL_AREA(gl_area), TRUE); - } - - if (screen != NULL) { - GdkVisual *visual = NULL; - gtk_widget_set_app_paintable(GTK_WIDGET(window), TRUE); - visual = gdk_screen_get_rgba_visual(screen); - if (visual != NULL && gdk_screen_is_composited(screen)) { - gtk_widget_set_visual(GTK_WIDGET(window), visual); - } - } -} diff --git a/libs/hbb_common/src/platform/linux.rs b/libs/hbb_common/src/platform/linux.rs index 31481ca78f23..60c8714d8212 100644 --- a/libs/hbb_common/src/platform/linux.rs +++ b/libs/hbb_common/src/platform/linux.rs @@ -13,35 +13,22 @@ pub const XDG_CURRENT_DESKTOP: &str = "XDG_CURRENT_DESKTOP"; pub struct Distro { pub name: String, - pub id: String, pub version_id: String, } impl Distro { fn new() -> Self { - // to-do: - // 1. Remove `run_cmds`, read file once - // 2. Add more distro infos let name = run_cmds("awk -F'=' '/^NAME=/ {print $2}' /etc/os-release") .unwrap_or_default() .trim() .trim_matches('"') .to_string(); - let id = run_cmds("awk -F'=' '/^ID=/ {print $2}' /etc/os-release") - .unwrap_or_default() - .trim() - .trim_matches('"') - .to_string(); let version_id = run_cmds("awk -F'=' '/^VERSION_ID=/ {print $2}' /etc/os-release") .unwrap_or_default() .trim() .trim_matches('"') .to_string(); - Self { - name, - id, - version_id, - } + Self { name, version_id } } } diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 5b6c48b95be2..088149a7e38b 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -19,7 +19,6 @@ use hbb_common::allow_err; use hbb_common::{ config::{self, LocalConfig, PeerConfig, PeerInfoSerde}, fs, lazy_static, log, - message_proto::Hash, rendezvous_proto::ConnType, ResultType, }; @@ -2343,25 +2342,6 @@ pub fn main_audio_support_loopback() -> SyncReturn { SyncReturn(is_surpport) } -pub fn get_os_distro_info() -> SyncReturn { - #[cfg(target_os = "linux")] - { - let distro = &hbb_common::platform::linux::DISTRO; - SyncReturn( - serde_json::to_string(&HashMap::from([ - ("name", distro.name.clone()), - ("id", distro.id.clone()), - ("version_id", distro.version_id.clone()), - ])) - .unwrap_or_default(), - ) - } - #[cfg(not(target_os = "linux"))] - { - SyncReturn("".to_owned()) - } -} - #[cfg(target_os = "android")] pub mod server_side { use hbb_common::{config, log}; From d5c5825ffdaabf6b465a86d3c1345ba0e2341957 Mon Sep 17 00:00:00 2001 From: Dmytro Zozulia <67293930+Nollasko@users.noreply.github.com> Date: Thu, 5 Dec 2024 05:04:10 +0200 Subject: [PATCH 450/541] Update uk.rs (#10174) --- src/lang/uk.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lang/uk.rs b/src/lang/uk.rs index 1a8a981e3665..e4d82563bbc1 100644 --- a/src/lang/uk.rs +++ b/src/lang/uk.rs @@ -246,7 +246,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Paste", "Вставити"), ("Paste here?", "Вставити сюди?"), ("Are you sure to close the connection?", "Ви впевнені, що хочете завершити підключення?"), - ("Download new version", "Отримайте нову версію"), + ("Download new version", "Завантажити нову версію"), ("Touch mode", "Сенсорний режим"), ("Mouse mode", "Режим миші"), ("One-Finger Tap", "Дотик одним пальцем"), From 468bdd6cc6f18f5b07a38e82f4b0d1c97acd051d Mon Sep 17 00:00:00 2001 From: RustDesk <71636191+rustdesk@users.noreply.github.com> Date: Thu, 5 Dec 2024 12:08:58 +0900 Subject: [PATCH 451/541] Revert "fix: workaround, linux window, transparent rounded corner (#10128)" (#10186) This reverts commit 8d4c86fe7ff96e38c526c910e8381d697ab66446. --- flutter/linux/my_application.cc | 45 ++------------------------------- flutter/pubspec.lock | 2 +- 2 files changed, 3 insertions(+), 44 deletions(-) diff --git a/flutter/linux/my_application.cc b/flutter/linux/my_application.cc index f4247bd94613..56b85ccae6dd 100644 --- a/flutter/linux/my_application.cc +++ b/flutter/linux/my_application.cc @@ -16,8 +16,6 @@ G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) extern bool gIsConnectionManager; -GtkWidget *find_gl_area(GtkWidget *widget); - // Implements GApplication::activate. static void my_application_activate(GApplication* application) { MyApplication* self = MY_APPLICATION(application); @@ -41,10 +39,9 @@ static void my_application_activate(GApplication* application) { // If running on Wayland assume the header bar will work (may need changing // if future cases occur). gboolean use_header_bar = TRUE; - GdkScreen* screen = NULL; #ifdef GDK_WINDOWING_X11 - screen = gtk_window_get_screen(window); - if (screen != NULL && GDK_IS_X11_SCREEN(screen)) { + GdkScreen* screen = gtk_window_get_screen(window); + if (GDK_IS_X11_SCREEN(screen)) { const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); if (g_strcmp0(wm_name, "GNOME Shell") != 0) { use_header_bar = FALSE; @@ -79,22 +76,6 @@ static void my_application_activate(GApplication* application) { gtk_widget_show(GTK_WIDGET(view)); gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); - // https://github.com/flutter/flutter/issues/152154 - // Remove this workaround when flutter version is updated. - GtkWidget *gl_area = find_gl_area(GTK_WIDGET(view)); - if (gl_area != NULL) { - gtk_gl_area_set_has_alpha(GTK_GL_AREA(gl_area), TRUE); - } - - if (screen != NULL) { - GdkVisual *visual = NULL; - gtk_widget_set_app_paintable(GTK_WIDGET(window), TRUE); - visual = gdk_screen_get_rgba_visual(screen); - if (visual != NULL && gdk_screen_is_composited(screen)) { - gtk_widget_set_visual(GTK_WIDGET(window), visual); - } - } - fl_register_plugins(FL_PLUGIN_REGISTRY(view)); gtk_widget_grab_focus(GTK_WIDGET(view)); @@ -140,25 +121,3 @@ MyApplication* my_application_new() { "flags", G_APPLICATION_NON_UNIQUE, nullptr)); } - -GtkWidget *find_gl_area(GtkWidget *widget) -{ - if (GTK_IS_GL_AREA(widget)) { - return widget; - } - - if (GTK_IS_CONTAINER(widget)) { - GList *children = gtk_container_get_children(GTK_CONTAINER(widget)); - for (GList *iter = children; iter != NULL; iter = g_list_next(iter)) { - GtkWidget *child = GTK_WIDGET(iter->data); - GtkWidget *gl_area = find_gl_area(child); - if (gl_area != NULL) { - g_list_free(children); - return gl_area; - } - } - g_list_free(children); - } - - return NULL; -} diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index 58df59a5521f..98f3eef96353 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -335,7 +335,7 @@ packages: description: path: "." ref: HEAD - resolved-ref: "4f562ab49d289cfa36bfda7cff12746ec0200033" + resolved-ref: "519350f1f40746798299e94786197d058353bac9" url: "https://github.com/rustdesk-org/rustdesk_desktop_multi_window" source: git version: "0.1.0" From 93e31078810ae5044837c1ee85b287f162a53e39 Mon Sep 17 00:00:00 2001 From: RustDesk <71636191+rustdesk@users.noreply.github.com> Date: Thu, 5 Dec 2024 18:07:23 +0900 Subject: [PATCH 452/541] =?UTF-8?q?Revert=20"Revert=20"fix:=20workaround,?= =?UTF-8?q?=20linux=20window,=20transparent=20rounded=20corner=20(#1?= =?UTF-8?q?=E2=80=A6"=20(#10191)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 468bdd6cc6f18f5b07a38e82f4b0d1c97acd051d. --- flutter/linux/my_application.cc | 45 +++++++++++++++++++++++++++++++-- flutter/pubspec.lock | 2 +- 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/flutter/linux/my_application.cc b/flutter/linux/my_application.cc index 56b85ccae6dd..f4247bd94613 100644 --- a/flutter/linux/my_application.cc +++ b/flutter/linux/my_application.cc @@ -16,6 +16,8 @@ G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) extern bool gIsConnectionManager; +GtkWidget *find_gl_area(GtkWidget *widget); + // Implements GApplication::activate. static void my_application_activate(GApplication* application) { MyApplication* self = MY_APPLICATION(application); @@ -39,9 +41,10 @@ static void my_application_activate(GApplication* application) { // If running on Wayland assume the header bar will work (may need changing // if future cases occur). gboolean use_header_bar = TRUE; + GdkScreen* screen = NULL; #ifdef GDK_WINDOWING_X11 - GdkScreen* screen = gtk_window_get_screen(window); - if (GDK_IS_X11_SCREEN(screen)) { + screen = gtk_window_get_screen(window); + if (screen != NULL && GDK_IS_X11_SCREEN(screen)) { const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); if (g_strcmp0(wm_name, "GNOME Shell") != 0) { use_header_bar = FALSE; @@ -76,6 +79,22 @@ static void my_application_activate(GApplication* application) { gtk_widget_show(GTK_WIDGET(view)); gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); + // https://github.com/flutter/flutter/issues/152154 + // Remove this workaround when flutter version is updated. + GtkWidget *gl_area = find_gl_area(GTK_WIDGET(view)); + if (gl_area != NULL) { + gtk_gl_area_set_has_alpha(GTK_GL_AREA(gl_area), TRUE); + } + + if (screen != NULL) { + GdkVisual *visual = NULL; + gtk_widget_set_app_paintable(GTK_WIDGET(window), TRUE); + visual = gdk_screen_get_rgba_visual(screen); + if (visual != NULL && gdk_screen_is_composited(screen)) { + gtk_widget_set_visual(GTK_WIDGET(window), visual); + } + } + fl_register_plugins(FL_PLUGIN_REGISTRY(view)); gtk_widget_grab_focus(GTK_WIDGET(view)); @@ -121,3 +140,25 @@ MyApplication* my_application_new() { "flags", G_APPLICATION_NON_UNIQUE, nullptr)); } + +GtkWidget *find_gl_area(GtkWidget *widget) +{ + if (GTK_IS_GL_AREA(widget)) { + return widget; + } + + if (GTK_IS_CONTAINER(widget)) { + GList *children = gtk_container_get_children(GTK_CONTAINER(widget)); + for (GList *iter = children; iter != NULL; iter = g_list_next(iter)) { + GtkWidget *child = GTK_WIDGET(iter->data); + GtkWidget *gl_area = find_gl_area(child); + if (gl_area != NULL) { + g_list_free(children); + return gl_area; + } + } + g_list_free(children); + } + + return NULL; +} diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index 98f3eef96353..58df59a5521f 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -335,7 +335,7 @@ packages: description: path: "." ref: HEAD - resolved-ref: "519350f1f40746798299e94786197d058353bac9" + resolved-ref: "4f562ab49d289cfa36bfda7cff12746ec0200033" url: "https://github.com/rustdesk-org/rustdesk_desktop_multi_window" source: git version: "0.1.0" From 2ce9b108ed66fe324cdb21becdb7e60ae9d2198c Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Thu, 5 Dec 2024 17:26:34 +0800 Subject: [PATCH 453/541] fix: linux, transparent window (#10192) Signed-off-by: fufesou --- flutter/linux/my_application.cc | 47 +++++++++++++++++++++------------ 1 file changed, 30 insertions(+), 17 deletions(-) diff --git a/flutter/linux/my_application.cc b/flutter/linux/my_application.cc index f4247bd94613..b9d36a0ce8d9 100644 --- a/flutter/linux/my_application.cc +++ b/flutter/linux/my_application.cc @@ -14,6 +14,9 @@ struct _MyApplication { G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) +GtkWidget *find_gl_area(GtkWidget *widget); +void try_set_transparent(GtkWindow* window, GdkScreen* screen, FlView* view); + extern bool gIsConnectionManager; GtkWidget *find_gl_area(GtkWidget *widget); @@ -69,31 +72,18 @@ static void my_application_activate(GApplication* application) { height = 490; } gtk_window_set_default_size(window, width, height); // <-- comment this line - gtk_widget_show(GTK_WIDGET(window)); + // gtk_widget_show(GTK_WIDGET(window)); gtk_widget_set_opacity(GTK_WIDGET(window), 0); g_autoptr(FlDartProject) project = fl_dart_project_new(); fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments); FlView* view = fl_view_new(project); - gtk_widget_show(GTK_WIDGET(view)); gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); - // https://github.com/flutter/flutter/issues/152154 - // Remove this workaround when flutter version is updated. - GtkWidget *gl_area = find_gl_area(GTK_WIDGET(view)); - if (gl_area != NULL) { - gtk_gl_area_set_has_alpha(GTK_GL_AREA(gl_area), TRUE); - } - - if (screen != NULL) { - GdkVisual *visual = NULL; - gtk_widget_set_app_paintable(GTK_WIDGET(window), TRUE); - visual = gdk_screen_get_rgba_visual(screen); - if (visual != NULL && gdk_screen_is_composited(screen)) { - gtk_widget_set_visual(GTK_WIDGET(window), visual); - } - } + try_set_transparent(window, gtk_window_get_screen(window), view); + gtk_widget_show(GTK_WIDGET(window)); + gtk_widget_show(GTK_WIDGET(view)); fl_register_plugins(FL_PLUGIN_REGISTRY(view)); @@ -162,3 +152,26 @@ GtkWidget *find_gl_area(GtkWidget *widget) return NULL; } + +// https://github.com/flutter/flutter/issues/152154 +// Remove this workaround when flutter version is updated. +void try_set_transparent(GtkWindow* window, GdkScreen* screen, FlView* view) +{ + GtkWidget *gl_area = NULL; + + printf("Try setting transparent\n"); + + gl_area = find_gl_area(GTK_WIDGET(view)); + if (gl_area != NULL) { + gtk_gl_area_set_has_alpha(GTK_GL_AREA(gl_area), TRUE); + } + + if (screen != NULL) { + GdkVisual *visual = NULL; + gtk_widget_set_app_paintable(GTK_WIDGET(window), TRUE); + visual = gdk_screen_get_rgba_visual(screen); + if (visual != NULL && gdk_screen_is_composited(screen)) { + gtk_widget_set_visual(GTK_WIDGET(window), visual); + } + } +} From 588103c6dc36e16d07347dbc7f15e7566ec3dcee Mon Sep 17 00:00:00 2001 From: rustdesk Date: Thu, 5 Dec 2024 18:38:39 +0800 Subject: [PATCH 454/541] 1.3.5 --- .github/workflows/flutter-build.yml | 4 ++-- .github/workflows/playground.yml | 2 +- Cargo.lock | 4 ++-- Cargo.toml | 2 +- appimage/AppImageBuilder-aarch64.yml | 2 +- appimage/AppImageBuilder-x86_64.yml | 2 +- flutter/pubspec.yaml | 2 +- libs/portable/Cargo.toml | 2 +- res/PKGBUILD | 2 +- res/rpm-flutter-suse.spec | 2 +- res/rpm-flutter.spec | 2 +- res/rpm.spec | 2 +- 12 files changed, 14 insertions(+), 14 deletions(-) diff --git a/.github/workflows/flutter-build.yml b/.github/workflows/flutter-build.yml index 200fb92f7123..9ec7d9e012d4 100644 --- a/.github/workflows/flutter-build.yml +++ b/.github/workflows/flutter-build.yml @@ -33,7 +33,7 @@ env: VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite" # vcpkg version: 2024.07.12 VCPKG_COMMIT_ID: "1de2026f28ead93ff1773e6e680387643e914ea1" - VERSION: "1.3.4" + VERSION: "1.3.5" NDK_VERSION: "r27c" #signing keys env variable checks ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}" @@ -719,7 +719,7 @@ jobs: shell: bash run: | cd "$(dirname "$(which flutter)")" - # https://github.com/flutter/flutter/issues/1.3.43 + # https://github.com/flutter/flutter/issues/1.3.53 sed -i -e 's/_setFramesEnabledState(false);/\/\/_setFramesEnabledState(false);/g' ../packages/flutter/lib/src/scheduler/binding.dart grep -n '_setFramesEnabledState(false);' ../packages/flutter/lib/src/scheduler/binding.dart diff --git a/.github/workflows/playground.yml b/.github/workflows/playground.yml index 282f678e8304..d60d6f7b1370 100644 --- a/.github/workflows/playground.yml +++ b/.github/workflows/playground.yml @@ -18,7 +18,7 @@ env: VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite" # vcpkg version: 2024.06.15 VCPKG_COMMIT_ID: "f7423ee180c4b7f40d43402c2feb3859161ef625" - VERSION: "1.3.4" + VERSION: "1.3.5" NDK_VERSION: "r26d" #signing keys env variable checks ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}" diff --git a/Cargo.lock b/Cargo.lock index b4ee9dd2d4b6..bc618d760298 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5494,7 +5494,7 @@ dependencies = [ [[package]] name = "rustdesk" -version = "1.3.4" +version = "1.3.5" dependencies = [ "android-wakelock", "android_logger", @@ -5594,7 +5594,7 @@ dependencies = [ [[package]] name = "rustdesk-portable-packer" -version = "1.3.4" +version = "1.3.5" dependencies = [ "brotli", "dirs 5.0.1", diff --git a/Cargo.toml b/Cargo.toml index 3cc05b780551..5949b5925856 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rustdesk" -version = "1.3.4" +version = "1.3.5" authors = ["rustdesk "] edition = "2021" build= "build.rs" diff --git a/appimage/AppImageBuilder-aarch64.yml b/appimage/AppImageBuilder-aarch64.yml index 21ddd3f3d886..98ceca3bb5ea 100644 --- a/appimage/AppImageBuilder-aarch64.yml +++ b/appimage/AppImageBuilder-aarch64.yml @@ -18,7 +18,7 @@ AppDir: id: rustdesk name: rustdesk icon: rustdesk - version: 1.3.4 + version: 1.3.5 exec: usr/lib/rustdesk/rustdesk exec_args: $@ apt: diff --git a/appimage/AppImageBuilder-x86_64.yml b/appimage/AppImageBuilder-x86_64.yml index 0f4b6b7e32f9..9ce7cc717e33 100644 --- a/appimage/AppImageBuilder-x86_64.yml +++ b/appimage/AppImageBuilder-x86_64.yml @@ -18,7 +18,7 @@ AppDir: id: rustdesk name: rustdesk icon: rustdesk - version: 1.3.4 + version: 1.3.5 exec: usr/lib/rustdesk/rustdesk exec_args: $@ apt: diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index 88e9dda1b874..4580af9d3f2e 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # 1.1.9-1 works for android, but for ios it becomes 1.1.91, need to set it to 1.1.9-a.1 for iOS, will get 1.1.9.1, but iOS store not allow 4 numbers -version: 1.3.4+53 +version: 1.3.5+54 environment: sdk: '^3.1.0' diff --git a/libs/portable/Cargo.toml b/libs/portable/Cargo.toml index 3cada5a19022..b9c4447a2138 100644 --- a/libs/portable/Cargo.toml +++ b/libs/portable/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rustdesk-portable-packer" -version = "1.3.4" +version = "1.3.5" edition = "2021" description = "RustDesk Remote Desktop" diff --git a/res/PKGBUILD b/res/PKGBUILD index c9a4eb19656d..79061a41b05c 100644 --- a/res/PKGBUILD +++ b/res/PKGBUILD @@ -1,5 +1,5 @@ pkgname=rustdesk -pkgver=1.3.4 +pkgver=1.3.5 pkgrel=0 epoch= pkgdesc="" diff --git a/res/rpm-flutter-suse.spec b/res/rpm-flutter-suse.spec index 433d37973b83..5686eea7abfa 100644 --- a/res/rpm-flutter-suse.spec +++ b/res/rpm-flutter-suse.spec @@ -1,5 +1,5 @@ Name: rustdesk -Version: 1.3.4 +Version: 1.3.5 Release: 0 Summary: RPM package License: GPL-3.0 diff --git a/res/rpm-flutter.spec b/res/rpm-flutter.spec index 8d7de5637b1b..8862614ea32e 100644 --- a/res/rpm-flutter.spec +++ b/res/rpm-flutter.spec @@ -1,5 +1,5 @@ Name: rustdesk -Version: 1.3.4 +Version: 1.3.5 Release: 0 Summary: RPM package License: GPL-3.0 diff --git a/res/rpm.spec b/res/rpm.spec index 1e8652140ae9..8d204eef2794 100644 --- a/res/rpm.spec +++ b/res/rpm.spec @@ -1,5 +1,5 @@ Name: rustdesk -Version: 1.3.4 +Version: 1.3.5 Release: 0 Summary: RPM package License: GPL-3.0 From 12e15b5a37e61f7a22f271ec1a3fb97bd0cea30d Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Fri, 6 Dec 2024 20:01:11 +0800 Subject: [PATCH 455/541] fix: linux, weak network, repeated keys (#10211) Use `press` as the `click` flag on Linux to avoid repeated keys, like the Legacy mode. Signed-off-by: fufesou --- libs/hbb_common/protos/message.proto | 2 ++ src/client.rs | 1 + src/keyboard.rs | 40 +++++++++++----------------- src/server/connection.rs | 30 ++++++++++++--------- 4 files changed, 35 insertions(+), 38 deletions(-) diff --git a/libs/hbb_common/protos/message.proto b/libs/hbb_common/protos/message.proto index 090b378a22cc..d4601c0f98e9 100644 --- a/libs/hbb_common/protos/message.proto +++ b/libs/hbb_common/protos/message.proto @@ -284,7 +284,9 @@ enum ControlKey { } message KeyEvent { + // `down` indicates the key's state(down or up). bool down = 1; + // `press` indicates a click event(down and up). bool press = 2; oneof union { ControlKey control_key = 3; diff --git a/src/client.rs b/src/client.rs index 4ff2c6b522b6..9a050c1cb13a 100644 --- a/src/client.rs +++ b/src/client.rs @@ -2748,6 +2748,7 @@ fn _input_os_password(p: String, activate: bool, interface: impl Interface) { return; } let mut key_event = KeyEvent::new(); + key_event.mode = KeyboardMode::Legacy.into(); key_event.press = true; let mut msg_out = Message::new(); key_event.set_seq(p); diff --git a/src/keyboard.rs b/src/keyboard.rs index 6c68dfa100f6..bc9ec8a771bc 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -417,6 +417,17 @@ pub fn is_modifier(key: &rdev::Key) -> bool { ) } +#[inline] +pub fn is_modifier_code(evt: &KeyEvent) -> bool { + match evt.union { + Some(key_event::Union::Chr(code)) => { + let key = rdev::linux_key_from_code(code); + is_modifier(&key) + } + _ => false, + } +} + #[inline] pub fn is_numpad_rdev_key(key: &rdev::Key) -> bool { matches!( @@ -869,24 +880,11 @@ pub fn legacy_keyboard_mode(event: &Event, mut key_event: KeyEvent) -> Vec Vec { - match _map_keyboard_mode(_peer, event, key_event) { - Some(key_event) => { - if _peer == OS_LOWER_LINUX { - if let EventType::KeyPress(k) = &event.event_type { - #[cfg(target_os = "ios")] - let try_workaround = true; - #[cfg(not(target_os = "ios"))] - let try_workaround = !is_modifier(k); - if try_workaround { - return try_workaround_linux_long_press(key_event); - } - } - } - vec![key_event] - } - None => Vec::new(), - } + _map_keyboard_mode(_peer, event, key_event) + .map(|e| vec![e]) + .unwrap_or_default() } fn _map_keyboard_mode(_peer: &str, event: &Event, mut key_event: KeyEvent) -> Option { @@ -958,14 +956,6 @@ fn _map_keyboard_mode(_peer: &str, event: &Event, mut key_event: KeyEvent) -> Op Some(key_event) } -// https://github.com/rustdesk/rustdesk/issues/6793 -#[inline] -fn try_workaround_linux_long_press(key_event: KeyEvent) -> Vec { - let mut key_event_up = key_event.clone(); - key_event_up.down = false; - vec![key_event, key_event_up] -} - #[cfg(not(any(target_os = "ios")))] fn try_fill_unicode(_peer: &str, event: &Event, key_event: &KeyEvent, events: &mut Vec) { match &event.unicode { diff --git a/src/server/connection.rs b/src/server/connection.rs index 1aa7d7e8ac49..e1c564ce6b7a 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -793,12 +793,13 @@ impl Connection { handle_mouse(&msg, id); } MessageInput::Key((mut msg, press)) => { - // todo: press and down have similar meanings. - if press && msg.mode.enum_value() == Ok(KeyboardMode::Legacy) { + // Set the press state to false, use `down` only in `handle_key()`. + msg.press = false; + if press { msg.down = true; } handle_key(&msg); - if press && msg.mode.enum_value() == Ok(KeyboardMode::Legacy) { + if press { msg.down = false; handle_key(&msg); } @@ -1965,8 +1966,6 @@ impl Connection { Some(message::Union::KeyEvent(..)) => {} #[cfg(any(target_os = "android"))] Some(message::Union::KeyEvent(mut me)) => { - let is_press = (me.press || me.down) && !crate::is_modifier(&me); - let key = match me.mode.enum_value() { Ok(KeyboardMode::Map) => { Some(crate::keyboard::keycode_to_rdev_key(me.chr())) @@ -1982,6 +1981,9 @@ impl Connection { } .filter(crate::keyboard::is_modifier); + let is_press = + (me.press || me.down) && !(crate::is_modifier(&me) || key.is_some()); + if let Some(key) = key { if is_press { self.pressed_modifiers.insert(key); @@ -2022,14 +2024,6 @@ impl Connection { } // https://github.com/rustdesk/rustdesk/issues/8633 MOUSE_MOVE_TIME.store(get_time(), Ordering::SeqCst); - // handle all down as press - // fix unexpected repeating key on remote linux, seems also fix abnormal alt/shift, which - // make sure all key are released - let is_press = if cfg!(target_os = "linux") { - (me.press || me.down) && !crate::is_modifier(&me) - } else { - me.press - }; let key = match me.mode.enum_value() { Ok(KeyboardMode::Map) => { @@ -2046,6 +2040,16 @@ impl Connection { } .filter(crate::keyboard::is_modifier); + // handle all down as press + // fix unexpected repeating key on remote linux, seems also fix abnormal alt/shift, which + // make sure all key are released + // https://github.com/rustdesk/rustdesk/issues/6793 + let is_press = if cfg!(target_os = "linux") { + (me.press || me.down) && !(crate::is_modifier(&me) || key.is_some()) + } else { + me.press + }; + if let Some(key) = key { if is_press { self.pressed_modifiers.insert(key); From 8f44787ba356b9f0efbc33bdae88ae306dffb15b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?VenusGirl=E2=9D=A4?= Date: Sat, 7 Dec 2024 11:24:32 +0900 Subject: [PATCH 456/541] Update README-KR.md (#10217) --- docs/README-KR.md | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/docs/README-KR.md b/docs/README-KR.md index af3c665244f3..b0a8b973e2ad 100644 --- a/docs/README-KR.md +++ b/docs/README-KR.md @@ -9,12 +9,12 @@ README를 모국어로 번역하기 위한 당신의 도움의 필요합니다.

    -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) -Rust로 작성되었고, 설정없이 바로 사용할 수 있는 원격 데스트탑 소프트웨어입니다. 자신의 데이터를 완전히 컨트롤할 수 있고, 보안의 염려도 없습니다. 우리의 rendezvous/relay 서버를 사용해도, [스스로 설정](https://rustdesk.com/server)하는 것도, [스스로 rendezvous/relay 서버를 작성할 수도 있습니다](https://github.com/rustdesk/rustdesk-server-demo). +Rust로 작성되었고, 설정없이 바로 사용할 수 있는 원격 데스트탑 소프트웨어입니다. 자신의 데이터를 완전히 컨트롤할 수 있고, 보안의 염려도 없습니다. 우리의 rendezvous/relay 서버를 사용해도, [직접 설정](https://rustdesk.com/server)하거나 [직접 rendezvous/relay 서버를 작성할 수도 있습니다](https://github.com/rustdesk/rustdesk-server-demo). ![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png) @@ -43,9 +43,9 @@ RustDesk는 모든 기여를 환영합니다. 기여하고자 한다면 [`docs/C - 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` +- 실행 `cargo run` -## [Build](https://rustdesk.com/docs/en/dev/build/) +## [빌드](https://rustdesk.com/docs/en/dev/build/) ## Linux에서 빌드 순서 @@ -67,7 +67,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 ``` -### Install vcpkg +### vcpkg 설치 ```sh git clone https://github.com/microsoft/vcpkg @@ -79,7 +79,7 @@ export VCPKG_ROOT=$HOME/vcpkg vcpkg/vcpkg install libvpx libyuv opus aom ``` -### Fix libvpx (For Fedora) +### libvpx 수정 (For Fedora용) ```sh cd vcpkg/buildtrees/libvpx/src @@ -92,7 +92,7 @@ cp libvpx.a $HOME/vcpkg/installed/x64-linux/lib/ cd ``` -### Build +### 빌드 ```sh curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh @@ -107,7 +107,7 @@ VCPKG_ROOT=$HOME/vcpkg cargo run ## Docker에 빌드하는 방법 -레포지토리를 클론하고, Docker 컨테이너 구성하는 것으로 시작합니다. +리포지토리를 클론하고, Docker 컨테이너 구성하는 것으로 시작합니다. ```sh git clone https://github.com/rustdesk/rustdesk @@ -115,13 +115,13 @@ cd rustdesk docker build -t "rustdesk-builder" . ``` -이후, 애플리케이션을 빌드할 필요가 있을 때마다, 이하의 커맨드를 실행합니다. +이후, 애플리케이션을 빌드할 필요가 있을 때마다, 아래의의 명령을 실행합니다. ```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 ``` -첫 빌드에서는 의존관계가 캐시될 때까지 시간이 걸릴 수 있습니다만, 이후의 빌드때는 빨라집니다. 더불어 빌드 커맨드에 다른 인수를 지정할 필요가 있다면, 커맨드 끝에 있는 `` 에 지정할 수 있습니다. 예를 들어 최적화된 출시 버전을 빌드하고 싶다면 이렇게 상기한 커맨드 뒤에 `--release` 를 붙여 실행합니다. 성공했다면 실행파일은 시스템 타겟 폴더에 담겨지고, 다음 커맨드로 실행할 수 있습니다. +첫 빌드에서는 의존관계가 캐시될 때까지 시간이 걸릴 수 있습니다만, 이후의 빌드때는 빨라집니다. 더불어 빌드 명령에 다른 인수를 지정할 필요가 있다면, 명령 끝에 있는 `` 에 지정할 수 있습니다. 예를 들어 최적화된 출시 버전을 빌드하고 싶다면 이렇게 상기한 명령 뒤에 `--release` 를 붙여 실행합니다. 성공했다면 실행파일은 시스템 타겟 폴더에 담겨지고, 다음 명령으로 실행할 수 있습니다. ```sh target/debug/rustdesk @@ -133,9 +133,9 @@ target/debug/rustdesk target/release/rustdesk ``` -커맨드를 RustDesk 리포지토리 루트에서 실행한다는 것을 확인해주세요. 그렇게 하지 않으면 애플리케이션이 필요한 리소스를 발견하지 못 할 가능성이 있습니다. 또한 `install`, `run` 같은 cargo 서브커맨드는 호스트가 아니라 컨테이너 프로그램을 설치, 실행을 위함이므로 현재 이 방법은 지원하지 않다는 점을 유념해주시길 바랍니다. +명령을 RustDesk 리포지토리 루트에서 실행한다는 것을 확인해주세요. 그렇게 하지 않으면 애플리케이션이 필요한 리소스를 발견하지 못 할 가능성이 있습니다. 또한 `install`, `run` 같은 cargo 하위 명령은 호스트가 아니라 컨테이너 프로그램을 설치, 실행을 위함이므로 현재 이 방법은 지원하지 않다는 점을 유념해주시길 바랍니다. -## File Structure +## 파일 구조 - **[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)**: 화면 캡처 @@ -143,12 +143,12 @@ target/release/rustdesk - **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: GUI - **[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/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) 혹은 relayed 접속 +- **[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) 혹은 relayed 접속 - **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: 플랫폼 고유의 코드 -- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: Flutter code for mobile -- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: Javascript for Flutter web client +- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: 모바일용 Flutter 코드 +- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: Flutter 웹 클라이언트용 자바스크립트 -## Snapshot +## 스냅샷 ![image](https://user-images.githubusercontent.com/71636191/113112362-ae4deb80-923b-11eb-957d-ff88daad4f06.png) From 3c838e7a9278df9e3f6293ef6aada14322e3a3c9 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Sat, 7 Dec 2024 15:12:15 +0800 Subject: [PATCH 457/541] fix: Android, try sync clipboard on connecting (#10218) * fix: Android, try sync clipboard on connecting Signed-off-by: fufesou * Android, clipboard, more clear skip check Signed-off-by: fufesou * comments Signed-off-by: fufesou * comment todo: Android clipboard listener, callback twice Signed-off-by: fufesou * Android, clipboard, remove listner Signed-off-by: fufesou --------- Signed-off-by: fufesou --- .../flutter_hbb/FloatingWindowService.kt | 2 +- .../com/carriez/flutter_hbb/MainActivity.kt | 14 ++--- .../com/carriez/flutter_hbb/MainService.kt | 2 + .../carriez/flutter_hbb/RdClipboardManager.kt | 53 +++++-------------- flutter/lib/mobile/pages/remote_page.dart | 13 +++++ flutter/lib/mobile/pages/server_page.dart | 2 - flutter/lib/models/server_model.dart | 15 ------ libs/scrap/src/android/ffi.rs | 8 --- src/flutter_ffi.rs | 16 ------ src/server/clipboard_service.rs | 13 ----- src/server/connection.rs | 10 +++- src/ui_cm_interface.rs | 11 ---- 12 files changed, 41 insertions(+), 118 deletions(-) diff --git a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/FloatingWindowService.kt b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/FloatingWindowService.kt index c5da81c7c4db..42a1add7bebf 100644 --- a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/FloatingWindowService.kt +++ b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/FloatingWindowService.kt @@ -306,7 +306,7 @@ class FloatingWindowService : Service(), View.OnTouchListener { popupMenu.menu.add(0, idShowRustDesk, 0, translate("Show RustDesk")) // For host side, clipboard sync val idSyncClipboard = 1 - val isClipboardListenerEnabled = MainActivity.rdClipboardManager?.isListening ?: false + val isClipboardListenerEnabled = MainActivity.rdClipboardManager?.isCaptureStarted ?: false if (isClipboardListenerEnabled) { popupMenu.menu.add(0, idSyncClipboard, 0, translate("Update client clipboard")) } diff --git a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainActivity.kt b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainActivity.kt index 333ea9f14bd2..5c54c18fb821 100644 --- a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainActivity.kt +++ b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainActivity.kt @@ -103,7 +103,6 @@ class MainActivity : FlutterActivity() { mainService?.let { unbindService(serviceConnection) } - rdClipboardManager?.rustEnableServiceClipboard(false) super.onDestroy() } @@ -221,6 +220,10 @@ class MainActivity : FlutterActivity() { result.success(true) } + "try_sync_clipboard" -> { + rdClipboardManager?.syncClipboard(true) + result.success(true) + } GET_START_ON_BOOT_OPT -> { val prefs = getSharedPreferences(KEY_SHARED_PREFERENCES, MODE_PRIVATE) result.success(prefs.getBoolean(KEY_START_ON_BOOT_OPT, false)) @@ -402,13 +405,4 @@ class MainActivity : FlutterActivity() { super.onStart() stopService(Intent(this, FloatingWindowService::class.java)) } - - // For client side - // When swithing from other app to this app, try to sync clipboard. - override fun onWindowFocusChanged(hasFocus: Boolean) { - super.onWindowFocusChanged(hasFocus) - if (hasFocus) { - rdClipboardManager?.syncClipboard(true) - } - } } diff --git a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainService.kt b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainService.kt index 4c40e3349ed8..e9ec0975d1be 100644 --- a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainService.kt +++ b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainService.kt @@ -433,6 +433,7 @@ class MainService : Service() { checkMediaPermission() _isStart = true FFI.setFrameRawEnable("video",true) + MainActivity.rdClipboardManager?.setCaptureStarted(_isStart) return true } @@ -441,6 +442,7 @@ class MainService : Service() { Log.d(logTag, "Stop Capture") FFI.setFrameRawEnable("video",false) _isStart = false + MainActivity.rdClipboardManager?.setCaptureStarted(_isStart) // release video if (reuseVirtualDisplay) { // The virtual display video projection can be paused by calling `setSurface(null)`. diff --git a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/RdClipboardManager.kt b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/RdClipboardManager.kt index 0e098cb0831b..8c9d85028402 100644 --- a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/RdClipboardManager.kt +++ b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/RdClipboardManager.kt @@ -36,19 +36,19 @@ class RdClipboardManager(private val clipboardManager: ClipboardManager) { // though the `lastUpdatedClipData` will be set to null once. private var lastUpdatedClipData: ClipData? = null private var isClientEnabled = true; - private var _isListening = false; - val isListening: Boolean - get() = _isListening + private var _isCaptureStarted = false; - fun checkPrimaryClip(isClient: Boolean, isSync: Boolean) { + val isCaptureStarted: Boolean + get() = _isCaptureStarted + + fun checkPrimaryClip(isClient: Boolean) { val clipData = clipboardManager.primaryClip if (clipData != null && clipData.itemCount > 0) { // Only handle the first item in the clipboard for now. val clip = clipData.getItemAt(0) - val isHostSync = !isClient && isSync - // Ignore the `isClipboardDataEqual()` check if it's a host sync operation. - // Because it's a action manually triggered by the user. - if (!isHostSync) { + // Ignore the `isClipboardDataEqual()` check if it's a host operation. + // Because it's an action manually triggered by the user. + if (isClient) { if (lastUpdatedClipData != null && isClipboardDataEqual(clipData, lastUpdatedClipData!!)) { Log.d(logTag, "Clipboard data is the same as last update, ignore") return @@ -95,13 +95,6 @@ class RdClipboardManager(private val clipboardManager: ClipboardManager) { } } - private val clipboardListener = object : ClipboardManager.OnPrimaryClipChangedListener { - override fun onPrimaryClipChanged() { - Log.d(logTag, "onPrimaryClipChanged") - checkPrimaryClip(true, false) - } - } - private fun isSupportedMimeType(mimeType: String): Boolean { return supportedMimeTypes.contains(mimeType) } @@ -136,43 +129,23 @@ class RdClipboardManager(private val clipboardManager: ClipboardManager) { return true } - @Keep - fun rustEnableServiceClipboard(enable: Boolean) { - Log.d(logTag, "rustEnableServiceClipboard: enable: $enable, _isListening: $_isListening") - if (enable) { - if (!_isListening) { - clipboardManager.addPrimaryClipChangedListener(clipboardListener) - _isListening = true - } - } else { - if (_isListening) { - clipboardManager.removePrimaryClipChangedListener(clipboardListener) - _isListening = false - lastUpdatedClipData = null - } - } + fun setCaptureStarted(started: Boolean) { + _isCaptureStarted = started } @Keep fun rustEnableClientClipboard(enable: Boolean) { Log.d(logTag, "rustEnableClientClipboard: enable: $enable") isClientEnabled = enable - if (enable) { - lastUpdatedClipData = clipboardManager.primaryClip - } else { - lastUpdatedClipData = null - } + lastUpdatedClipData = null } fun syncClipboard(isClient: Boolean) { - Log.d(logTag, "syncClipboard: isClient: $isClient, isClientEnabled: $isClientEnabled, _isListening: $_isListening") + Log.d(logTag, "syncClipboard: isClient: $isClient, isClientEnabled: $isClientEnabled") if (isClient && !isClientEnabled) { return } - if (!isClient && !_isListening) { - return - } - checkPrimaryClip(isClient, true) + checkPrimaryClip(isClient) } @Keep diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index fa7c35bb64e2..003640e05e1a 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -147,6 +147,19 @@ class _RemotePageState extends State with WidgetsBindingObserver { gFFI.chatModel.onVoiceCallClosed("End connetion"); } + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + if (state == AppLifecycleState.resumed) { + trySyncClipboard(); + } + } + + // For client side + // When swithing from other app to this app, try to sync clipboard. + void trySyncClipboard() { + gFFI.invokeMethod("try_sync_clipboard"); + } + @override void didChangeMetrics() { // If the soft keyboard is visible and the canvas has been changed(panned or scaled) diff --git a/flutter/lib/mobile/pages/server_page.dart b/flutter/lib/mobile/pages/server_page.dart index db91e998b6ea..e9382be9f9f4 100644 --- a/flutter/lib/mobile/pages/server_page.dart +++ b/flutter/lib/mobile/pages/server_page.dart @@ -597,8 +597,6 @@ class _PermissionCheckerState extends State { style: const TextStyle(color: MyTheme.darkGray), )) ]), - PermissionRow(translate("Enable clipboard"), serverModel.clipboardOk, - serverModel.toggleClipboard), ])); } } diff --git a/flutter/lib/models/server_model.dart b/flutter/lib/models/server_model.dart index 7754672c0d0e..1d800ef69678 100644 --- a/flutter/lib/models/server_model.dart +++ b/flutter/lib/models/server_model.dart @@ -30,7 +30,6 @@ class ServerModel with ChangeNotifier { bool _inputOk = false; bool _audioOk = false; bool _fileOk = false; - bool _clipboardOk = false; bool _showElevation = false; bool hideCm = false; int _connectStatus = 0; // Rendezvous Server status @@ -60,8 +59,6 @@ class ServerModel with ChangeNotifier { bool get fileOk => _fileOk; - bool get clipboardOk => _clipboardOk; - bool get showElevation => _showElevation; int get connectStatus => _connectStatus; @@ -212,10 +209,6 @@ class ServerModel with ChangeNotifier { _fileOk = fileOption != 'N'; } - // clipboard - final clipOption = await bind.mainGetOption(key: kOptionEnableClipboard); - _clipboardOk = clipOption != 'N'; - notifyListeners(); } @@ -322,14 +315,6 @@ class ServerModel with ChangeNotifier { notifyListeners(); } - toggleClipboard() async { - _clipboardOk = !_clipboardOk; - bind.mainSetOption( - key: kOptionEnableClipboard, - value: _clipboardOk ? defaultOptionYes : 'N'); - notifyListeners(); - } - toggleInput() async { if (clients.isNotEmpty) { await showClientsMayNotBeChangedAlert(parent.target); diff --git a/libs/scrap/src/android/ffi.rs b/libs/scrap/src/android/ffi.rs index ffa38a965d73..7433e6b090f9 100644 --- a/libs/scrap/src/android/ffi.rs +++ b/libs/scrap/src/android/ffi.rs @@ -371,14 +371,6 @@ pub fn call_clipboard_manager_update_clipboard(data: &[u8]) -> JniResult<()> { } } -pub fn call_clipboard_manager_enable_service_clipboard(enable: bool) -> JniResult<()> { - _call_clipboard_manager( - "rustEnableServiceClipboard", - "(Z)V", - &[JValue::Bool(jboolean::from(enable))], - ) -} - pub fn call_clipboard_manager_enable_client_clipboard(enable: bool) -> JniResult<()> { _call_clipboard_manager( "rustEnableClientClipboard", diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 088149a7e38b..7e3f39c83224 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -831,17 +831,6 @@ pub fn main_show_option(_key: String) -> SyncReturn { SyncReturn(false) } -#[inline] -#[cfg(target_os = "android")] -fn enable_server_clipboard(keyboard_enabled: &str, clip_enabled: &str) { - use scrap::android::ffi::call_clipboard_manager_enable_service_clipboard; - let keyboard_enabled = - config::option2bool(config::keys::OPTION_ENABLE_KEYBOARD, &keyboard_enabled); - let clip_enabled = config::option2bool(config::keys::OPTION_ENABLE_CLIPBOARD, &clip_enabled); - crate::ui_cm_interface::switch_permission_all("clipboard".to_owned(), clip_enabled); - let _ = call_clipboard_manager_enable_service_clipboard(keyboard_enabled && clip_enabled); -} - pub fn main_set_option(key: String, value: String) { #[cfg(target_os = "android")] if key.eq(config::keys::OPTION_ENABLE_KEYBOARD) { @@ -849,11 +838,6 @@ pub fn main_set_option(key: String, value: String) { config::keys::OPTION_ENABLE_KEYBOARD, &value, )); - enable_server_clipboard(&value, &get_option(config::keys::OPTION_ENABLE_CLIPBOARD)); - } - #[cfg(target_os = "android")] - if key.eq(config::keys::OPTION_ENABLE_CLIPBOARD) { - enable_server_clipboard(&get_option(config::keys::OPTION_ENABLE_KEYBOARD), &value); } if key.eq("custom-rendezvous-server") { set_option(key, value.clone()); diff --git a/src/server/clipboard_service.rs b/src/server/clipboard_service.rs index 401bb49336b4..bfba41c92a03 100644 --- a/src/server/clipboard_service.rs +++ b/src/server/clipboard_service.rs @@ -8,8 +8,6 @@ use crate::ipc::{self, ClipboardFile, ClipboardNonFile, Data}; use clipboard_master::{CallbackResult, ClipboardHandler}; #[cfg(target_os = "android")] use hbb_common::config::{keys, option2bool}; -#[cfg(target_os = "android")] -use scrap::android::ffi::call_clipboard_manager_enable_service_clipboard; use std::{ io, sync::mpsc::{channel, RecvTimeoutError, Sender}, @@ -224,24 +222,13 @@ impl Handler { } } -#[cfg(target_os = "android")] -fn is_clipboard_enabled() -> bool { - let keyboard_enabled = crate::ui_interface::get_option(keys::OPTION_ENABLE_KEYBOARD); - let keyboard_enabled = option2bool(keys::OPTION_ENABLE_KEYBOARD, &keyboard_enabled); - let clip_enabled = crate::ui_interface::get_option(keys::OPTION_ENABLE_CLIPBOARD); - let clip_enabled = option2bool(keys::OPTION_ENABLE_CLIPBOARD, &clip_enabled); - keyboard_enabled && clip_enabled -} - #[cfg(target_os = "android")] fn run(sp: EmptyExtraFieldService) -> ResultType<()> { - let _res = call_clipboard_manager_enable_service_clipboard(is_clipboard_enabled()); while sp.ok() { if let Some(msg) = crate::clipboard::get_clipboards_msg(false) { sp.send(msg); } std::thread::sleep(Duration::from_millis(INTERVAL)); } - let _res = call_clipboard_manager_enable_service_clipboard(false); Ok(()) } diff --git a/src/server/connection.rs b/src/server/connection.rs index e1c564ce6b7a..28f653fdec31 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -1372,8 +1372,14 @@ impl Connection { if !self.follow_remote_window { noperms.push(NAME_WINDOW_FOCUS); } - if !self.clipboard_enabled() - || !self.peer_keyboard_enabled() + // Do not consider the clipboard and keyboard permissions on Android. + // Because syncing the clipboard on Android is manually triggered by the user in the floating ball. + #[cfg(target_os = "android")] + let keyboard_clip_noperm = self.disable_keyboard || self.disable_clipboard; + #[cfg(not(target_os = "android"))] + let keyboard_clip_noperm = + !self.clipboard_enabled() || !self.peer_keyboard_enabled(); + if keyboard_clip_noperm || crate::get_builtin_option(keys::OPTION_ONE_WAY_CLIPBOARD_REDIRECTION) == "Y" { noperms.push(super::clipboard_service::NAME); diff --git a/src/ui_cm_interface.rs b/src/ui_cm_interface.rs index 2ff5e086287d..d2d5b2c83354 100644 --- a/src/ui_cm_interface.rs +++ b/src/ui_cm_interface.rs @@ -312,17 +312,6 @@ pub fn switch_permission(id: i32, name: String, enabled: bool) { }; } -#[inline] -#[cfg(target_os = "android")] -pub fn switch_permission_all(name: String, enabled: bool) { - for (_, client) in CLIENTS.read().unwrap().iter() { - allow_err!(client.tx.send(Data::SwitchPermission { - name: name.clone(), - enabled - })); - } -} - #[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))] #[inline] pub fn get_clients_state() -> String { From 1c17fddf519543d142af47f12f81eac20096a135 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Sat, 7 Dec 2024 22:34:54 +0800 Subject: [PATCH 458/541] fix: android clipboard permission (#10223) * fix: android clipboard permission Signed-off-by: fufesou * refact: Android, clipboard, floating ball Call rust to check if clipboard is enabled. Signed-off-by: fufesou --------- Signed-off-by: fufesou --- .../flutter_hbb/FloatingWindowService.kt | 4 +-- flutter/android/app/src/main/kotlin/ffi.kt | 1 + flutter/lib/mobile/pages/server_page.dart | 2 ++ flutter/lib/models/server_model.dart | 15 +++++++++ src/flutter_ffi.rs | 26 ++++++++++++--- src/ipc.rs | 2 -- src/server.rs | 2 ++ src/server/clipboard_service.rs | 12 +++++++ src/server/connection.rs | 32 ++++++++----------- src/ui_cm_interface.rs | 20 ++++++------ 10 files changed, 80 insertions(+), 36 deletions(-) diff --git a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/FloatingWindowService.kt b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/FloatingWindowService.kt index 42a1add7bebf..696d536c62c3 100644 --- a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/FloatingWindowService.kt +++ b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/FloatingWindowService.kt @@ -306,8 +306,8 @@ class FloatingWindowService : Service(), View.OnTouchListener { popupMenu.menu.add(0, idShowRustDesk, 0, translate("Show RustDesk")) // For host side, clipboard sync val idSyncClipboard = 1 - val isClipboardListenerEnabled = MainActivity.rdClipboardManager?.isCaptureStarted ?: false - if (isClipboardListenerEnabled) { + val isServiceSyncEnabled = (MainActivity.rdClipboardManager?.isCaptureStarted ?: false) && FFI.isServiceClipboardEnabled() + if (isServiceSyncEnabled) { popupMenu.menu.add(0, idSyncClipboard, 0, translate("Update client clipboard")) } val idStopService = 2 diff --git a/flutter/android/app/src/main/kotlin/ffi.kt b/flutter/android/app/src/main/kotlin/ffi.kt index 69b395ac2e9d..9f0f0216b727 100644 --- a/flutter/android/app/src/main/kotlin/ffi.kt +++ b/flutter/android/app/src/main/kotlin/ffi.kt @@ -24,4 +24,5 @@ object FFI { external fun setCodecInfo(info: String) external fun getLocalOption(key: String): String external fun onClipboardUpdate(clips: ByteBuffer) + external fun isServiceClipboardEnabled(): Boolean } diff --git a/flutter/lib/mobile/pages/server_page.dart b/flutter/lib/mobile/pages/server_page.dart index e9382be9f9f4..db91e998b6ea 100644 --- a/flutter/lib/mobile/pages/server_page.dart +++ b/flutter/lib/mobile/pages/server_page.dart @@ -597,6 +597,8 @@ class _PermissionCheckerState extends State { style: const TextStyle(color: MyTheme.darkGray), )) ]), + PermissionRow(translate("Enable clipboard"), serverModel.clipboardOk, + serverModel.toggleClipboard), ])); } } diff --git a/flutter/lib/models/server_model.dart b/flutter/lib/models/server_model.dart index 1d800ef69678..8775764619ee 100644 --- a/flutter/lib/models/server_model.dart +++ b/flutter/lib/models/server_model.dart @@ -30,6 +30,7 @@ class ServerModel with ChangeNotifier { bool _inputOk = false; bool _audioOk = false; bool _fileOk = false; + bool _clipboardOk = false; bool _showElevation = false; bool hideCm = false; int _connectStatus = 0; // Rendezvous Server status @@ -59,6 +60,8 @@ class ServerModel with ChangeNotifier { bool get fileOk => _fileOk; + bool get clipboardOk => _clipboardOk; + bool get showElevation => _showElevation; int get connectStatus => _connectStatus; @@ -209,6 +212,10 @@ class ServerModel with ChangeNotifier { _fileOk = fileOption != 'N'; } + // clipboard + final clipOption = await bind.mainGetOption(key: kOptionEnableClipboard); + _clipboardOk = clipOption != 'N'; + notifyListeners(); } @@ -315,6 +322,14 @@ class ServerModel with ChangeNotifier { notifyListeners(); } + toggleClipboard() async { + _clipboardOk = !clipboardOk; + bind.mainSetOption( + key: kOptionEnableClipboard, + value: clipboardOk ? defaultOptionYes : 'N'); + notifyListeners(); + } + toggleInput() async { if (clients.isNotEmpty) { await showClientsMayNotBeChangedAlert(parent.target); diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 7e3f39c83224..0bb17c9036dc 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -834,11 +834,19 @@ pub fn main_show_option(_key: String) -> SyncReturn { pub fn main_set_option(key: String, value: String) { #[cfg(target_os = "android")] if key.eq(config::keys::OPTION_ENABLE_KEYBOARD) { - crate::ui_cm_interface::notify_input_control(config::option2bool( - config::keys::OPTION_ENABLE_KEYBOARD, - &value, - )); + crate::ui_cm_interface::switch_permission_all( + "keyboard".to_owned(), + config::option2bool(&key, &value), + ); + } + #[cfg(target_os = "android")] + if key.eq(config::keys::OPTION_ENABLE_CLIPBOARD) { + crate::ui_cm_interface::switch_permission_all( + "clipboard".to_owned(), + config::option2bool(&key, &value), + ); } + if key.eq("custom-rendezvous-server") { set_option(key, value.clone()); #[cfg(target_os = "android")] @@ -2332,7 +2340,7 @@ pub mod server_side { use jni::{ errors::{Error as JniError, Result as JniResult}, objects::{JClass, JObject, JString}, - sys::jstring, + sys::{jboolean, jstring}, JNIEnv, }; @@ -2405,4 +2413,12 @@ pub mod server_side { }; return env.new_string(res).unwrap_or_default().into_raw(); } + + #[no_mangle] + pub unsafe extern "system" fn Java_ffi_FFI_isServiceClipboardEnabled( + env: JNIEnv, + _class: JClass, + ) -> jboolean { + jboolean::from(crate::server::is_clipboard_service_ok()) + } } diff --git a/src/ipc.rs b/src/ipc.rs index e3bcfac9a49f..5126aaf4e408 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -217,8 +217,6 @@ pub enum Data { MouseMoveTime(i64), Authorize, Close, - #[cfg(target_os = "android")] - InputControl(bool), #[cfg(windows)] SAS, UserSid(Option), diff --git a/src/server.rs b/src/server.rs index ed2c9f2fd541..ba1682f3d0f5 100644 --- a/src/server.rs +++ b/src/server.rs @@ -34,6 +34,8 @@ pub mod audio_service; cfg_if::cfg_if! { if #[cfg(not(target_os = "ios"))] { mod clipboard_service; +#[cfg(target_os = "android")] +pub use clipboard_service::is_clipboard_service_ok; #[cfg(target_os = "linux")] pub(crate) mod wayland; #[cfg(target_os = "linux")] diff --git a/src/server/clipboard_service.rs b/src/server/clipboard_service.rs index bfba41c92a03..8ae482500550 100644 --- a/src/server/clipboard_service.rs +++ b/src/server/clipboard_service.rs @@ -8,6 +8,8 @@ use crate::ipc::{self, ClipboardFile, ClipboardNonFile, Data}; use clipboard_master::{CallbackResult, ClipboardHandler}; #[cfg(target_os = "android")] use hbb_common::config::{keys, option2bool}; +#[cfg(target_os = "android")] +use std::sync::atomic::{AtomicBool, Ordering}; use std::{ io, sync::mpsc::{channel, RecvTimeoutError, Sender}, @@ -16,6 +18,9 @@ use std::{ #[cfg(windows)] use tokio::runtime::Runtime; +#[cfg(target_os = "android")] +static CLIPBOARD_SERVICE_OK: AtomicBool = AtomicBool::new(false); + #[cfg(not(target_os = "android"))] struct Handler { sp: EmptyExtraFieldService, @@ -27,6 +32,11 @@ struct Handler { rt: Option, } +#[cfg(target_os = "android")] +pub fn is_clipboard_service_ok() -> bool { + CLIPBOARD_SERVICE_OK.load(Ordering::SeqCst) +} + pub fn new() -> GenericService { let svc = EmptyExtraFieldService::new(NAME.to_owned(), false); GenericService::run(&svc.clone(), run); @@ -224,11 +234,13 @@ impl Handler { #[cfg(target_os = "android")] fn run(sp: EmptyExtraFieldService) -> ResultType<()> { + CLIPBOARD_SERVICE_OK.store(sp.ok(), Ordering::SeqCst); while sp.ok() { if let Some(msg) = crate::clipboard::get_clipboards_msg(false) { sp.send(msg); } std::thread::sleep(Duration::from_millis(INTERVAL)); } + CLIPBOARD_SERVICE_OK.store(false, Ordering::SeqCst); Ok(()) } diff --git a/src/server/connection.rs b/src/server/connection.rs index 28f653fdec31..153740c28a3d 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -463,11 +463,6 @@ impl Connection { conn.on_close("connection manager", true).await; break; } - #[cfg(target_os = "android")] - ipc::Data::InputControl(v) => { - conn.keyboard = v; - conn.send_permission(Permission::Keyboard, v).await; - } ipc::Data::CmErr(e) => { if e != "expected" { // cm closed before connection @@ -492,6 +487,9 @@ impl Connection { conn.keyboard = enabled; conn.send_permission(Permission::Keyboard, enabled).await; if let Some(s) = conn.server.upgrade() { + s.write().unwrap().subscribe( + super::clipboard_service::NAME, + conn.inner.clone(), conn.can_sub_clipboard_service()); s.write().unwrap().subscribe( NAME_CURSOR, conn.inner.clone(), enabled || conn.show_remote_cursor); @@ -502,7 +500,7 @@ impl Connection { if let Some(s) = conn.server.upgrade() { s.write().unwrap().subscribe( super::clipboard_service::NAME, - conn.inner.clone(), conn.clipboard_enabled() && conn.peer_keyboard_enabled()); + conn.inner.clone(), conn.can_sub_clipboard_service()); } } else if &name == "audio" { conn.audio = enabled; @@ -1372,16 +1370,7 @@ impl Connection { if !self.follow_remote_window { noperms.push(NAME_WINDOW_FOCUS); } - // Do not consider the clipboard and keyboard permissions on Android. - // Because syncing the clipboard on Android is manually triggered by the user in the floating ball. - #[cfg(target_os = "android")] - let keyboard_clip_noperm = self.disable_keyboard || self.disable_clipboard; - #[cfg(not(target_os = "android"))] - let keyboard_clip_noperm = - !self.clipboard_enabled() || !self.peer_keyboard_enabled(); - if keyboard_clip_noperm - || crate::get_builtin_option(keys::OPTION_ONE_WAY_CLIPBOARD_REDIRECTION) == "Y" - { + if !self.can_sub_clipboard_service() { noperms.push(super::clipboard_service::NAME); } if !self.audio_enabled() { @@ -1453,6 +1442,13 @@ impl Connection { self.clipboard && !self.disable_clipboard } + #[inline] + fn can_sub_clipboard_service(&self) -> bool { + self.clipboard_enabled() + && self.peer_keyboard_enabled() + && crate::get_builtin_option(keys::OPTION_ONE_WAY_CLIPBOARD_REDIRECTION) != "Y" + } + fn audio_enabled(&self) -> bool { self.audio && !self.disable_audio } @@ -2930,7 +2926,7 @@ impl Connection { s.write().unwrap().subscribe( super::clipboard_service::NAME, self.inner.clone(), - self.clipboard_enabled() && self.peer_keyboard_enabled(), + self.can_sub_clipboard_service(), ); } } @@ -2942,7 +2938,7 @@ impl Connection { s.write().unwrap().subscribe( super::clipboard_service::NAME, self.inner.clone(), - self.clipboard_enabled() && self.peer_keyboard_enabled(), + self.can_sub_clipboard_service(), ); s.write().unwrap().subscribe( NAME_CURSOR, diff --git a/src/ui_cm_interface.rs b/src/ui_cm_interface.rs index d2d5b2c83354..a3373f8ccd72 100644 --- a/src/ui_cm_interface.rs +++ b/src/ui_cm_interface.rs @@ -280,15 +280,6 @@ pub fn close(id: i32) { }; } -#[inline] -#[cfg(target_os = "android")] -pub fn notify_input_control(v: bool) { - for (_, mut client) in CLIENTS.write().unwrap().iter_mut() { - client.keyboard = v; - allow_err!(client.tx.send(Data::InputControl(v))); - } -} - #[inline] pub fn remove(id: i32) { CLIENTS.write().unwrap().remove(&id); @@ -312,6 +303,17 @@ pub fn switch_permission(id: i32, name: String, enabled: bool) { }; } +#[inline] +#[cfg(target_os = "android")] +pub fn switch_permission_all(name: String, enabled: bool) { + for (_, client) in CLIENTS.read().unwrap().iter() { + allow_err!(client.tx.send(Data::SwitchPermission { + name: name.clone(), + enabled + })); + } +} + #[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))] #[inline] pub fn get_clients_state() -> String { From d4a712bb320e16d59f6c6ecebe118a3b0ba17633 Mon Sep 17 00:00:00 2001 From: 21pages Date: Sun, 8 Dec 2024 18:26:55 +0800 Subject: [PATCH 459/541] always block desktop settings page if video connection exists (#10224) 1. Always block desktop settings page if video connection exists, both mouse event and key event are blocked.. 2. Server control page always block key event. Signed-off-by: 21pages --- flutter/lib/common.dart | 7 +- flutter/lib/consts.dart | 2 +- .../lib/desktop/pages/connection_page.dart | 5 +- .../lib/desktop/pages/desktop_home_page.dart | 23 ++++++- .../desktop/pages/desktop_setting_page.dart | 65 +++++++++++++++++-- .../lib/desktop/pages/desktop_tab_page.dart | 17 +---- flutter/lib/desktop/pages/server_page.dart | 24 +++++-- .../lib/desktop/widgets/tabbar_widget.dart | 17 +---- flutter/lib/models/state_model.dart | 1 + src/ipc.rs | 2 + src/ui_interface.rs | 16 ++++- 11 files changed, 126 insertions(+), 53 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 0dd8849f363e..208897ed0397 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -2809,7 +2809,7 @@ Widget buildRemoteBlock( onExit: (event) => block.value = false, child: Stack(children: [ // scope block tab - FocusScope(child: child, canRequestFocus: !block.value), + preventMouseKeyBuilder(child: child, block: block.value), // mask block click, cm not block click and still use check_click_time to avoid block local click if (mask) Offstage( @@ -2821,6 +2821,11 @@ Widget buildRemoteBlock( )); } +Widget preventMouseKeyBuilder({required Widget child, required bool block}) { + return ExcludeFocus( + excluding: block, child: AbsorbPointer(child: child, absorbing: block)); +} + Widget unreadMessageCountBuilder(RxInt? count, {double? size, double? fontSize}) { return Obx(() => Offstage( diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index f6f9c4d34f9a..95b207826a71 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -575,4 +575,4 @@ extension WindowsTargetExt on int { WindowsTarget get windowsVersion => getWindowsTarget(this); } -const kCheckSoftwareUpdateFinish = 'check_software_update_finish'; \ No newline at end of file +const kCheckSoftwareUpdateFinish = 'check_software_update_finish'; diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index e2681bb377d1..f2c7121016e1 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -179,6 +179,9 @@ class _OnlineStatusWidgetState extends State { stateGlobal.svcStatus.value = SvcStatus.notReady; } _svcIsUsingPublicServer.value = await bind.mainIsUsingPublicServer(); + try { + stateGlobal.videoConnCount.value = status['video_conn_count'] as int; + } catch (_) {} } } @@ -359,7 +362,7 @@ class _ConnectionPageState extends State ); } String textToFind = textEditingValue.text.toLowerCase(); - _autocompleteOpts = peers + _autocompleteOpts = peers .where((peer) => peer.id.toLowerCase().contains(textToFind) || peer.username diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index 754efbd5ae83..8061a45161e5 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -35,7 +35,7 @@ class DesktopHomePage extends StatefulWidget { const borderColor = Color(0xFF2F65BA); class _DesktopHomePageState extends State - with AutomaticKeepAliveClientMixin { + with AutomaticKeepAliveClientMixin, WidgetsBindingObserver { final _leftPaneScrollController = ScrollController(); @override @@ -51,6 +51,7 @@ class _DesktopHomePageState extends State bool isCardClosed = false; final RxBool _editHover = false.obs; + final RxBool _block = false.obs; final GlobalKey _childKey = GlobalKey(); @@ -58,14 +59,20 @@ class _DesktopHomePageState extends State Widget build(BuildContext context) { super.build(context); final isIncomingOnly = bind.isIncomingOnly(); - return Row( + return _buildBlock( + child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ buildLeftPane(context), if (!isIncomingOnly) const VerticalDivider(width: 1), if (!isIncomingOnly) Expanded(child: buildRightPane(context)), ], - ); + )); + } + + Widget _buildBlock({required Widget child}) { + return buildRemoteBlock( + block: _block, mask: true, use: canBeBlocked, child: child); } Widget buildLeftPane(BuildContext context) { @@ -805,6 +812,7 @@ class _DesktopHomePageState extends State _updateWindowSize(); }); } + WidgetsBinding.instance.addObserver(this); } _updateWindowSize() { @@ -830,9 +838,18 @@ class _DesktopHomePageState extends State platformFFI.unregisterEventHandler( kCheckSoftwareUpdateFinish, kCheckSoftwareUpdateFinish); } + WidgetsBinding.instance.removeObserver(this); super.dispose(); } + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + super.didChangeAppLifecycleState(state); + if (state == AppLifecycleState.resumed) { + shouldBeBlocked(_block, canBeBlocked); + } + } + Widget buildPluginEntry() { final entries = PluginUiManager.instance.entries.entries; return Offstage( diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 44cdbcbb81b2..56a99446c384 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -107,13 +107,20 @@ class DesktopSettingPage extends StatefulWidget { } class _DesktopSettingPageState extends State - with TickerProviderStateMixin, AutomaticKeepAliveClientMixin { + with + TickerProviderStateMixin, + AutomaticKeepAliveClientMixin, + WidgetsBindingObserver { late PageController controller; late Rx selectedTab; @override bool get wantKeepAlive => true; + final RxBool _block = false.obs; + final RxBool _canBeBlocked = false.obs; + Timer? _videoConnTimer; + _DesktopSettingPageState(SettingsTabKey initialTabkey) { var initialIndex = DesktopSettingPage.tabKeys.indexOf(initialTabkey); if (initialIndex == -1) { @@ -133,11 +140,34 @@ class _DesktopSettingPageState extends State }); } + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + super.didChangeAppLifecycleState(state); + if (state == AppLifecycleState.resumed) { + shouldBeBlocked(_block, canBeBlocked); + } + } + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addObserver(this); + _videoConnTimer = + periodic_immediate(Duration(milliseconds: 1000), () async { + if (!mounted) { + return; + } + _canBeBlocked.value = await canBeBlocked(); + }); + } + @override void dispose() { super.dispose(); Get.delete(tag: _kSettingPageControllerTag); Get.delete(tag: _kSettingPageTabKeyTag); + WidgetsBinding.instance.removeObserver(this); + _videoConnTimer?.cancel(); } List<_TabInfo> _settingTabs() { @@ -207,12 +237,35 @@ class _DesktopSettingPageState extends State return children; } + Widget _buildBlock({required List children}) { + // check both mouseMoveTime and videoConnCount + return Obx(() { + final videoConnBlock = + _canBeBlocked.value && stateGlobal.videoConnCount > 0; + return Stack(children: [ + buildRemoteBlock( + block: _block, + mask: false, + use: canBeBlocked, + child: preventMouseKeyBuilder( + child: Row(children: children), + block: videoConnBlock, + ), + ), + if (videoConnBlock) + Container( + color: Colors.black.withOpacity(0.5), + ) + ]); + }); + } + @override Widget build(BuildContext context) { super.build(context); return Scaffold( backgroundColor: Theme.of(context).colorScheme.background, - body: Row( + body: _buildBlock( children: [ SizedBox( width: _kTabWidth, @@ -706,8 +759,8 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { locked = false; setState(() => {}); }), - AbsorbPointer( - absorbing: locked, + preventMouseKeyBuilder( + block: locked, child: Column(children: [ permissions(context), password(context), @@ -1374,8 +1427,8 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin { locked = false; setState(() => {}); }), - AbsorbPointer( - absorbing: locked, + preventMouseKeyBuilder( + block: locked, child: Column(children: [ network(context), ]), diff --git a/flutter/lib/desktop/pages/desktop_tab_page.dart b/flutter/lib/desktop/pages/desktop_tab_page.dart index 7319f7a3c158..6440e55a1117 100644 --- a/flutter/lib/desktop/pages/desktop_tab_page.dart +++ b/flutter/lib/desktop/pages/desktop_tab_page.dart @@ -37,13 +37,9 @@ class DesktopTabPage extends StatefulWidget { } } -class _DesktopTabPageState extends State - with WidgetsBindingObserver { +class _DesktopTabPageState extends State { final tabController = DesktopTabController(tabType: DesktopTabType.main); - final RxBool _block = false.obs; - // bool mouseIn = false; - _DesktopTabPageState() { RemoteCountState.init(); Get.put(tabController); @@ -69,19 +65,10 @@ class _DesktopTabPageState extends State } } - @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); } /* @@ -97,7 +84,6 @@ class _DesktopTabPageState extends State @override void dispose() { // HardwareKeyboard.instance.removeHandler(_handleKeyEvent); - WidgetsBinding.instance.removeObserver(this); Get.delete(); super.dispose(); @@ -119,7 +105,6 @@ class _DesktopTabPageState extends State isClose: false, ), ), - blockTab: _block, ))); return isMacOS || kUseCompatibleUiMode ? tabWidget diff --git a/flutter/lib/desktop/pages/server_page.dart b/flutter/lib/desktop/pages/server_page.dart index 53ba73c972f4..95d9f2c7c7d5 100644 --- a/flutter/lib/desktop/pages/server_page.dart +++ b/flutter/lib/desktop/pages/server_page.dart @@ -110,7 +110,8 @@ class ConnectionManager extends StatefulWidget { class ConnectionManagerState extends State with WidgetsBindingObserver { - final RxBool _block = false.obs; + final RxBool _controlPageBlock = false.obs; + final RxBool _sidePageBlock = false.obs; ConnectionManagerState() { gFFI.serverModel.tabController.onSelected = (client_id_str) { @@ -139,7 +140,8 @@ class ConnectionManagerState extends State super.didChangeAppLifecycleState(state); if (state == AppLifecycleState.resumed) { if (!allowRemoteCMModification()) { - shouldBeBlocked(_block, null); + shouldBeBlocked(_controlPageBlock, null); + shouldBeBlocked(_sidePageBlock, null); } } } @@ -192,7 +194,6 @@ class ConnectionManagerState extends State selectedBorderColor: MyTheme.accent, maxLabelWidth: 100, tail: null, //buildScrollJumper(), - blockTab: allowRemoteCMModification() ? null : _block, tabBuilder: (key, icon, label, themeConf) { final client = serverModel.clients .firstWhereOrNull((client) => client.id.toString() == key); @@ -237,13 +238,20 @@ class ConnectionManagerState extends State ? buildSidePage() : buildRemoteBlock( child: buildSidePage(), - block: _block, + block: _sidePageBlock, mask: true), )), SizedBox( width: realClosedWidth, - child: - SizedBox(width: realClosedWidth, child: pageView)), + child: SizedBox( + width: realClosedWidth, + child: allowRemoteCMModification() + ? pageView + : buildRemoteBlock( + child: _buildKeyEventBlock(pageView), + block: _controlPageBlock, + mask: false, + ))), ]); return Container( color: Theme.of(context).scaffoldBackgroundColor, @@ -268,6 +276,10 @@ class ConnectionManagerState extends State } } + Widget _buildKeyEventBlock(Widget child) { + return ExcludeFocus(child: child, excluding: true); + } + Widget buildTitleBar() { return SizedBox( height: kDesktopRemoteTabBarHeight, diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index 75ecacbfe580..96ada22c9072 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -246,7 +246,6 @@ class DesktopTab extends StatefulWidget { final Color? selectedTabBackgroundColor; final Color? unSelectedTabBackgroundColor; final Color? selectedBorderColor; - final RxBool? blockTab; final DesktopTabController controller; @@ -272,7 +271,6 @@ class DesktopTab extends StatefulWidget { this.selectedTabBackgroundColor, this.unSelectedTabBackgroundColor, this.selectedBorderColor, - this.blockTab, }) : super(key: key); static RxString tablabelGetter(String peerId) { @@ -311,7 +309,6 @@ class _DesktopTabState extends State Color? get unSelectedTabBackgroundColor => widget.unSelectedTabBackgroundColor; Color? get selectedBorderColor => widget.selectedBorderColor; - RxBool? get blockTab => widget.blockTab; DesktopTabController get controller => widget.controller; RxList get invisibleTabKeys => widget.invisibleTabKeys; Debouncer get _scrollDebounce => widget._scrollDebounce; @@ -533,21 +530,9 @@ class _DesktopTabState extends State ]); } - Widget _buildBlock({required Widget child}) { - if (blockTab != null) { - return buildRemoteBlock( - child: child, - block: blockTab!, - use: canBeBlocked, - mask: tabType == DesktopTabType.main); - } else { - return child; - } - } - List _tabWidgets = []; Widget _buildPageView() { - final child = _buildBlock( + final child = Container( child: Obx(() => PageView( controller: state.value.pageController, physics: NeverScrollableScrollPhysics(), diff --git a/flutter/lib/models/state_model.dart b/flutter/lib/models/state_model.dart index f096036498e9..2e1b516df01a 100644 --- a/flutter/lib/models/state_model.dart +++ b/flutter/lib/models/state_model.dart @@ -18,6 +18,7 @@ class StateGlobal { final RxDouble _windowBorderWidth = RxDouble(kWindowBorderWidth); final RxBool showRemoteToolBar = false.obs; final svcStatus = SvcStatus.notReady.obs; + final RxInt videoConnCount = 0.obs; final RxBool isFocused = false.obs; // for mobile and web bool isInMainPage = true; diff --git a/src/ipc.rs b/src/ipc.rs index 5126aaf4e408..f1deb5ba8e5c 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -266,6 +266,7 @@ pub enum Data { ControlledSessionCount(usize), CmErr(String), CheckHwcodec, + #[cfg(feature = "flutter")] VideoConnCount(Option), // Although the key is not neccessary, it is used to avoid hardcoding the key. WaylandScreencastRestoreToken((String, String)), @@ -455,6 +456,7 @@ async fn handle(data: Data, stream: &mut Connection) { log::info!("socks updated"); } }, + #[cfg(feature = "flutter")] Data::VideoConnCount(None) => { let n = crate::server::AUTHED_CONNS .lock() diff --git a/src/ui_interface.rs b/src/ui_interface.rs index 5d7f9ee039cd..323b651fedb3 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -47,6 +47,8 @@ pub struct UiStatus { pub mouse_time: i64, #[cfg(not(feature = "flutter"))] pub id: String, + #[cfg(feature = "flutter")] + pub video_conn_count: usize, } #[derive(Debug, Clone, Serialize)] @@ -65,14 +67,14 @@ lazy_static::lazy_static! { mouse_time: 0, #[cfg(not(feature = "flutter"))] id: "".to_owned(), + #[cfg(feature = "flutter")] + video_conn_count: 0, })); static ref ASYNC_JOB_STATUS : Arc> = Default::default(); static ref ASYNC_HTTP_STATUS : Arc>> = Arc::new(Mutex::new(HashMap::new())); static ref TEMPORARY_PASSWD : Arc> = Arc::new(Mutex::new("".to_owned())); } -pub static VIDEO_CONN_COUNT: AtomicUsize = AtomicUsize::new(0); - #[cfg(not(any(target_os = "android", target_os = "ios")))] lazy_static::lazy_static! { static ref OPTION_SYNCED: Arc> = Default::default(); @@ -1144,6 +1146,8 @@ async fn check_connect_status_(reconnect: bool, rx: mpsc::UnboundedReceiver { - VIDEO_CONN_COUNT.store(n, Ordering::Relaxed); + video_conn_count = n; } Ok(Some(ipc::Data::OnlineStatus(Some((mut x, _c))))) => { if x > 0 { @@ -1223,6 +1228,8 @@ async fn check_connect_status_(reconnect: bool, rx: mpsc::UnboundedReceiver {} @@ -1236,6 +1243,7 @@ async fn check_connect_status_(reconnect: bool, rx: mpsc::UnboundedReceiver Date: Sun, 8 Dec 2024 18:27:45 +0800 Subject: [PATCH 460/541] add version update translation (#10225) Signed-off-by: 21pages --- flutter/lib/desktop/pages/desktop_home_page.dart | 2 +- src/lang/ar.rs | 1 + src/lang/be.rs | 1 + src/lang/bg.rs | 1 + src/lang/ca.rs | 1 + src/lang/cn.rs | 1 + src/lang/cs.rs | 1 + src/lang/da.rs | 1 + src/lang/de.rs | 1 + src/lang/el.rs | 1 + src/lang/en.rs | 1 + src/lang/eo.rs | 1 + src/lang/es.rs | 1 + src/lang/et.rs | 1 + src/lang/eu.rs | 1 + src/lang/fa.rs | 1 + src/lang/fr.rs | 1 + src/lang/he.rs | 1 + src/lang/hr.rs | 1 + src/lang/hu.rs | 1 + src/lang/id.rs | 1 + src/lang/it.rs | 1 + src/lang/ja.rs | 1 + src/lang/ko.rs | 1 + src/lang/kz.rs | 1 + src/lang/lt.rs | 1 + src/lang/lv.rs | 1 + src/lang/nb.rs | 1 + src/lang/nl.rs | 1 + src/lang/pl.rs | 1 + src/lang/pt_PT.rs | 1 + src/lang/ptbr.rs | 1 + src/lang/ro.rs | 1 + src/lang/ru.rs | 1 + src/lang/sk.rs | 1 + src/lang/sl.rs | 1 + src/lang/sq.rs | 1 + src/lang/sr.rs | 1 + src/lang/sv.rs | 1 + src/lang/template.rs | 1 + src/lang/th.rs | 1 + src/lang/tr.rs | 1 + src/lang/tw.rs | 1 + src/lang/uk.rs | 1 + src/lang/vn.rs | 1 + 45 files changed, 45 insertions(+), 1 deletion(-) diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index 8061a45161e5..23bd3dc3faf9 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -430,7 +430,7 @@ class _DesktopHomePageState extends State bind.mainUriPrefixSync().contains('rustdesk')) { return buildInstallCard( "Status", - "There is a newer version of ${bind.mainGetAppNameSync()} ${bind.mainGetNewVersion()} available.", + "${translate("newer-version-of-{${bind.mainGetAppNameSync()}}-tip")} (${bind.mainGetNewVersion()}).", "Click to download", () async { final Uri url = Uri.parse('https://rustdesk.com/download'); await launchUrl(url); diff --git a/src/lang/ar.rs b/src/lang/ar.rs index 1b20ebc4a99a..6eda9246a00f 100644 --- a/src/lang/ar.rs +++ b/src/lang/ar.rs @@ -655,5 +655,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is synchronized", ""), ("Update client clipboard", ""), ("Untagged", ""), + ("newer-version-of-{}-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/be.rs b/src/lang/be.rs index 7c081983782a..90f1aa0fe699 100644 --- a/src/lang/be.rs +++ b/src/lang/be.rs @@ -655,5 +655,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is synchronized", ""), ("Update client clipboard", ""), ("Untagged", ""), + ("newer-version-of-{}-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/bg.rs b/src/lang/bg.rs index 3c1d202eeb41..94dfc3529a8d 100644 --- a/src/lang/bg.rs +++ b/src/lang/bg.rs @@ -655,5 +655,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is synchronized", ""), ("Update client clipboard", ""), ("Untagged", ""), + ("newer-version-of-{}-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ca.rs b/src/lang/ca.rs index 120200b3520c..8d9eaae47999 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -655,5 +655,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is synchronized", ""), ("Update client clipboard", ""), ("Untagged", ""), + ("newer-version-of-{}-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 901b4cdb90ff..ed7328a0f6dc 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -655,5 +655,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is synchronized", "剪贴板已同步"), ("Update client clipboard", "更新客户端的粘贴板"), ("Untagged", "无标签"), + ("newer-version-of-{}-tip", "{} 版本更新"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index 25046cbcb40e..f67a0c5276f4 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -655,5 +655,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is synchronized", ""), ("Update client clipboard", ""), ("Untagged", ""), + ("newer-version-of-{}-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index fa3e6f100697..6eafbccffe89 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -655,5 +655,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is synchronized", ""), ("Update client clipboard", ""), ("Untagged", ""), + ("newer-version-of-{}-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index 482b45bfc79b..b35b2141adea 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -655,5 +655,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is synchronized", "Zwischenablage ist synchronisiert"), ("Update client clipboard", "Client-Zwischenablage aktualisieren"), ("Untagged", ""), + ("newer-version-of-{}-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/el.rs b/src/lang/el.rs index 57984b7288f0..c463d9f738a5 100644 --- a/src/lang/el.rs +++ b/src/lang/el.rs @@ -655,5 +655,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is synchronized", ""), ("Update client clipboard", ""), ("Untagged", ""), + ("newer-version-of-{}-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/en.rs b/src/lang/en.rs index 7ed83a8fe4f0..cb3d599dc5ab 100644 --- a/src/lang/en.rs +++ b/src/lang/en.rs @@ -236,5 +236,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("enable-trusted-devices-tip", "Skip 2FA verification on trusted devices"), ("one-way-file-transfer-tip", "One-way file transfer is enabled on the controlled side."), ("web_id_input_tip", "You can input an ID in the same server, direct IP access is not supported in web client.\nIf you want to access a device on another server, please append the server address (@?key=), for example,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nIf you want to access a device on a public server, please input \"@public\", the key is not needed for public server."), + ("newer-version-of-{}-tip", "There is a newer version of {} available"), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 5fe2c8d3a65b..1acd6889e8b3 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -655,5 +655,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is synchronized", ""), ("Update client clipboard", ""), ("Untagged", ""), + ("newer-version-of-{}-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index 4b84b1c0f46a..692463619e8c 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -655,5 +655,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is synchronized", "Portapapeles sincronizado"), ("Update client clipboard", "Actualizar portapapeles del cliente"), ("Untagged", ""), + ("newer-version-of-{}-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/et.rs b/src/lang/et.rs index 931a3da2d3e5..dcd90f7259a8 100644 --- a/src/lang/et.rs +++ b/src/lang/et.rs @@ -655,5 +655,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is synchronized", ""), ("Update client clipboard", ""), ("Untagged", ""), + ("newer-version-of-{}-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eu.rs b/src/lang/eu.rs index e191a74f09c9..9cf484a5bf6e 100644 --- a/src/lang/eu.rs +++ b/src/lang/eu.rs @@ -655,5 +655,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is synchronized", ""), ("Update client clipboard", ""), ("Untagged", ""), + ("newer-version-of-{}-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index 051859f60eab..64caa142d4af 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -655,5 +655,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is synchronized", ""), ("Update client clipboard", ""), ("Untagged", ""), + ("newer-version-of-{}-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 774e03865c2f..cd9d3b529228 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -655,5 +655,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is synchronized", ""), ("Update client clipboard", ""), ("Untagged", ""), + ("newer-version-of-{}-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/he.rs b/src/lang/he.rs index 39cd98fc4c10..7c38c803fc6f 100644 --- a/src/lang/he.rs +++ b/src/lang/he.rs @@ -655,5 +655,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is synchronized", ""), ("Update client clipboard", ""), ("Untagged", ""), + ("newer-version-of-{}-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hr.rs b/src/lang/hr.rs index d3163ae03c7e..47d19796c6ba 100644 --- a/src/lang/hr.rs +++ b/src/lang/hr.rs @@ -655,5 +655,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is synchronized", ""), ("Update client clipboard", ""), ("Untagged", ""), + ("newer-version-of-{}-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index c348db1d1d60..39dda41d99f9 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -655,5 +655,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is synchronized", "A vágólap szinkronizálva van"), ("Update client clipboard", ""), ("Untagged", ""), + ("newer-version-of-{}-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index d52c11a384e0..a23cac7083e4 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -655,5 +655,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is synchronized", ""), ("Update client clipboard", ""), ("Untagged", ""), + ("newer-version-of-{}-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index 838df4d99726..068e03bf49e3 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -655,5 +655,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is synchronized", "Gli appunti sono sincronizzati"), ("Update client clipboard", "Aggiorna appunti client"), ("Untagged", "Senza tag"), + ("newer-version-of-{}-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 14c06e0d59bc..cb5168725d72 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -655,5 +655,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is synchronized", ""), ("Update client clipboard", ""), ("Untagged", ""), + ("newer-version-of-{}-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 93c174894119..e5d0017a28c3 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -655,5 +655,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is synchronized", "클립보드가 동기화됨"), ("Update client clipboard", "클라이언트 클립보드 업데이트"), ("Untagged", "태그 없음"), + ("newer-version-of-{}-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index f9ee96ca2518..4eaf3d767f91 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -655,5 +655,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is synchronized", ""), ("Update client clipboard", ""), ("Untagged", ""), + ("newer-version-of-{}-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lt.rs b/src/lang/lt.rs index 31522364c1b5..45ee545b1ff3 100644 --- a/src/lang/lt.rs +++ b/src/lang/lt.rs @@ -655,5 +655,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is synchronized", ""), ("Update client clipboard", ""), ("Untagged", ""), + ("newer-version-of-{}-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lv.rs b/src/lang/lv.rs index 31a8d89c02c8..f84b2ca51cc5 100644 --- a/src/lang/lv.rs +++ b/src/lang/lv.rs @@ -655,5 +655,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is synchronized", "Starpliktuve ir sinhronizēta"), ("Update client clipboard", "Atjaunināt klienta starpliktuvi"), ("Untagged", "Neatzīmēts"), + ("newer-version-of-{}-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nb.rs b/src/lang/nb.rs index 00ee513b14c0..ccaf9ee9af0c 100644 --- a/src/lang/nb.rs +++ b/src/lang/nb.rs @@ -655,5 +655,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is synchronized", ""), ("Update client clipboard", ""), ("Untagged", ""), + ("newer-version-of-{}-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nl.rs b/src/lang/nl.rs index f1c7ba97f9b1..e1d889e0c19a 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -655,5 +655,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is synchronized", "Klembord is gesynchroniseerd"), ("Update client clipboard", "Klembord van client bijwerken"), ("Untagged", ""), + ("newer-version-of-{}-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 872fd5bf713b..5ed4bfb397a6 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -655,5 +655,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is synchronized", "Schowek jest zsynchronizowany"), ("Update client clipboard", "Uaktualnij schowek klienta"), ("Untagged", ""), + ("newer-version-of-{}-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index ad3a5f83171a..43cafa6be518 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -655,5 +655,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is synchronized", ""), ("Update client clipboard", ""), ("Untagged", ""), + ("newer-version-of-{}-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index 466952c023e0..c1f96e452e11 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -655,5 +655,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is synchronized", ""), ("Update client clipboard", ""), ("Untagged", ""), + ("newer-version-of-{}-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ro.rs b/src/lang/ro.rs index 830a26a4929a..9070b39accab 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -655,5 +655,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is synchronized", ""), ("Update client clipboard", ""), ("Untagged", ""), + ("newer-version-of-{}-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 5979961ddf91..6ce56881352a 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -655,5 +655,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is synchronized", "Буфер обмена синхронизирован"), ("Update client clipboard", "Обновить буфер обмена клиента"), ("Untagged", "Без метки"), + ("newer-version-of-{}-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index d6bd0f7111f4..bfb1dd1ffd93 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -655,5 +655,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is synchronized", ""), ("Update client clipboard", ""), ("Untagged", ""), + ("newer-version-of-{}-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sl.rs b/src/lang/sl.rs index 3fbf30ba3011..af40e973f0c1 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -655,5 +655,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is synchronized", ""), ("Update client clipboard", ""), ("Untagged", ""), + ("newer-version-of-{}-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sq.rs b/src/lang/sq.rs index b68e6a1e03bb..aa29aef0d3a1 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -655,5 +655,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is synchronized", ""), ("Update client clipboard", ""), ("Untagged", ""), + ("newer-version-of-{}-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sr.rs b/src/lang/sr.rs index 9def539e59a9..3549c572b497 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -655,5 +655,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is synchronized", ""), ("Update client clipboard", ""), ("Untagged", ""), + ("newer-version-of-{}-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sv.rs b/src/lang/sv.rs index af00b788ed73..de170cc26114 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -655,5 +655,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is synchronized", ""), ("Update client clipboard", ""), ("Untagged", ""), + ("newer-version-of-{}-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index ce3e99abdee0..0240534c872e 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -655,5 +655,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is synchronized", ""), ("Update client clipboard", ""), ("Untagged", ""), + ("newer-version-of-{}-tip", "There is a newer version of {} available"), ].iter().cloned().collect(); } diff --git a/src/lang/th.rs b/src/lang/th.rs index 09fde8671f81..4a6107284d1a 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -655,5 +655,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is synchronized", ""), ("Update client clipboard", ""), ("Untagged", ""), + ("newer-version-of-{}-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index b7f9750f1e8b..f7bebb0142aa 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -655,5 +655,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is synchronized", ""), ("Update client clipboard", ""), ("Untagged", ""), + ("newer-version-of-{}-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 8a42a331182f..d75e12add769 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -655,5 +655,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is synchronized", "剪貼簿已同步"), ("Update client clipboard", "更新客戶端的剪貼簿"), ("Untagged", "無標籤"), + ("newer-version-of-{}-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/uk.rs b/src/lang/uk.rs index e4d82563bbc1..805db1afe0a3 100644 --- a/src/lang/uk.rs +++ b/src/lang/uk.rs @@ -655,5 +655,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is synchronized", "Буфер обміну синхронізовано"), ("Update client clipboard", "Оновити буфер обміну клієнта"), ("Untagged", "Без міток"), + ("newer-version-of-{}-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index 1970e17ca9c9..81cf265d81cf 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -655,5 +655,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is synchronized", ""), ("Update client clipboard", ""), ("Untagged", ""), + ("newer-version-of-{}-tip", ""), ].iter().cloned().collect(); } From b39e851262cea9dd869dcaae55a1f6730728f086 Mon Sep 17 00:00:00 2001 From: 21pages Date: Sun, 8 Dec 2024 20:09:10 +0800 Subject: [PATCH 461/541] fix typo (#10227) newer version -> new version Signed-off-by: 21pages --- flutter/lib/desktop/pages/desktop_home_page.dart | 2 +- src/lang/ar.rs | 2 +- src/lang/be.rs | 2 +- src/lang/bg.rs | 2 +- src/lang/ca.rs | 2 +- src/lang/cn.rs | 2 +- src/lang/cs.rs | 2 +- src/lang/da.rs | 2 +- src/lang/de.rs | 2 +- src/lang/el.rs | 2 +- src/lang/en.rs | 2 +- src/lang/eo.rs | 2 +- src/lang/es.rs | 2 +- src/lang/et.rs | 2 +- src/lang/eu.rs | 2 +- src/lang/fa.rs | 2 +- src/lang/fr.rs | 2 +- src/lang/he.rs | 2 +- src/lang/hr.rs | 2 +- src/lang/hu.rs | 2 +- src/lang/id.rs | 2 +- src/lang/it.rs | 2 +- src/lang/ja.rs | 2 +- src/lang/ko.rs | 2 +- src/lang/kz.rs | 2 +- src/lang/lt.rs | 2 +- src/lang/lv.rs | 2 +- src/lang/nb.rs | 2 +- src/lang/nl.rs | 2 +- src/lang/pl.rs | 2 +- src/lang/pt_PT.rs | 2 +- src/lang/ptbr.rs | 2 +- src/lang/ro.rs | 2 +- src/lang/ru.rs | 2 +- src/lang/sk.rs | 2 +- src/lang/sl.rs | 2 +- src/lang/sq.rs | 2 +- src/lang/sr.rs | 2 +- src/lang/sv.rs | 2 +- src/lang/template.rs | 2 +- src/lang/th.rs | 2 +- src/lang/tr.rs | 2 +- src/lang/tw.rs | 2 +- src/lang/uk.rs | 2 +- src/lang/vn.rs | 2 +- 45 files changed, 45 insertions(+), 45 deletions(-) diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index 23bd3dc3faf9..10f5cc4fdbae 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -430,7 +430,7 @@ class _DesktopHomePageState extends State bind.mainUriPrefixSync().contains('rustdesk')) { return buildInstallCard( "Status", - "${translate("newer-version-of-{${bind.mainGetAppNameSync()}}-tip")} (${bind.mainGetNewVersion()}).", + "${translate("new-version-of-{${bind.mainGetAppNameSync()}}-tip")} (${bind.mainGetNewVersion()}).", "Click to download", () async { final Uri url = Uri.parse('https://rustdesk.com/download'); await launchUrl(url); diff --git a/src/lang/ar.rs b/src/lang/ar.rs index 6eda9246a00f..08eae5dbd5b0 100644 --- a/src/lang/ar.rs +++ b/src/lang/ar.rs @@ -655,6 +655,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is synchronized", ""), ("Update client clipboard", ""), ("Untagged", ""), - ("newer-version-of-{}-tip", ""), + ("new-version-of-{}-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/be.rs b/src/lang/be.rs index 90f1aa0fe699..c23143776090 100644 --- a/src/lang/be.rs +++ b/src/lang/be.rs @@ -655,6 +655,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is synchronized", ""), ("Update client clipboard", ""), ("Untagged", ""), - ("newer-version-of-{}-tip", ""), + ("new-version-of-{}-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/bg.rs b/src/lang/bg.rs index 94dfc3529a8d..469b1b4fb39e 100644 --- a/src/lang/bg.rs +++ b/src/lang/bg.rs @@ -655,6 +655,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is synchronized", ""), ("Update client clipboard", ""), ("Untagged", ""), - ("newer-version-of-{}-tip", ""), + ("new-version-of-{}-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ca.rs b/src/lang/ca.rs index 8d9eaae47999..33992a3386bb 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -655,6 +655,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is synchronized", ""), ("Update client clipboard", ""), ("Untagged", ""), - ("newer-version-of-{}-tip", ""), + ("new-version-of-{}-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index ed7328a0f6dc..a3e3666c680b 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -655,6 +655,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is synchronized", "剪贴板已同步"), ("Update client clipboard", "更新客户端的粘贴板"), ("Untagged", "无标签"), - ("newer-version-of-{}-tip", "{} 版本更新"), + ("new-version-of-{}-tip", "{} 版本更新"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index f67a0c5276f4..e36c493618bf 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -655,6 +655,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is synchronized", ""), ("Update client clipboard", ""), ("Untagged", ""), - ("newer-version-of-{}-tip", ""), + ("new-version-of-{}-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index 6eafbccffe89..7988da242e74 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -655,6 +655,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is synchronized", ""), ("Update client clipboard", ""), ("Untagged", ""), - ("newer-version-of-{}-tip", ""), + ("new-version-of-{}-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index b35b2141adea..23ec9247ed80 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -655,6 +655,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is synchronized", "Zwischenablage ist synchronisiert"), ("Update client clipboard", "Client-Zwischenablage aktualisieren"), ("Untagged", ""), - ("newer-version-of-{}-tip", ""), + ("new-version-of-{}-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/el.rs b/src/lang/el.rs index c463d9f738a5..0b78aa685603 100644 --- a/src/lang/el.rs +++ b/src/lang/el.rs @@ -655,6 +655,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is synchronized", ""), ("Update client clipboard", ""), ("Untagged", ""), - ("newer-version-of-{}-tip", ""), + ("new-version-of-{}-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/en.rs b/src/lang/en.rs index cb3d599dc5ab..2295fd05cef7 100644 --- a/src/lang/en.rs +++ b/src/lang/en.rs @@ -236,6 +236,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("enable-trusted-devices-tip", "Skip 2FA verification on trusted devices"), ("one-way-file-transfer-tip", "One-way file transfer is enabled on the controlled side."), ("web_id_input_tip", "You can input an ID in the same server, direct IP access is not supported in web client.\nIf you want to access a device on another server, please append the server address (@?key=), for example,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nIf you want to access a device on a public server, please input \"@public\", the key is not needed for public server."), - ("newer-version-of-{}-tip", "There is a newer version of {} available"), + ("new-version-of-{}-tip", "There is a new version of {} available"), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 1acd6889e8b3..7370c2429fba 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -655,6 +655,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is synchronized", ""), ("Update client clipboard", ""), ("Untagged", ""), - ("newer-version-of-{}-tip", ""), + ("new-version-of-{}-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index 692463619e8c..a3437e01f0b5 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -655,6 +655,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is synchronized", "Portapapeles sincronizado"), ("Update client clipboard", "Actualizar portapapeles del cliente"), ("Untagged", ""), - ("newer-version-of-{}-tip", ""), + ("new-version-of-{}-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/et.rs b/src/lang/et.rs index dcd90f7259a8..b38b55bd615d 100644 --- a/src/lang/et.rs +++ b/src/lang/et.rs @@ -655,6 +655,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is synchronized", ""), ("Update client clipboard", ""), ("Untagged", ""), - ("newer-version-of-{}-tip", ""), + ("new-version-of-{}-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eu.rs b/src/lang/eu.rs index 9cf484a5bf6e..efb281496df0 100644 --- a/src/lang/eu.rs +++ b/src/lang/eu.rs @@ -655,6 +655,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is synchronized", ""), ("Update client clipboard", ""), ("Untagged", ""), - ("newer-version-of-{}-tip", ""), + ("new-version-of-{}-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index 64caa142d4af..d1d3d47679a6 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -655,6 +655,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is synchronized", ""), ("Update client clipboard", ""), ("Untagged", ""), - ("newer-version-of-{}-tip", ""), + ("new-version-of-{}-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index cd9d3b529228..19b4e58a63b9 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -655,6 +655,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is synchronized", ""), ("Update client clipboard", ""), ("Untagged", ""), - ("newer-version-of-{}-tip", ""), + ("new-version-of-{}-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/he.rs b/src/lang/he.rs index 7c38c803fc6f..d877f0226820 100644 --- a/src/lang/he.rs +++ b/src/lang/he.rs @@ -655,6 +655,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is synchronized", ""), ("Update client clipboard", ""), ("Untagged", ""), - ("newer-version-of-{}-tip", ""), + ("new-version-of-{}-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hr.rs b/src/lang/hr.rs index 47d19796c6ba..ba4723b8a2f3 100644 --- a/src/lang/hr.rs +++ b/src/lang/hr.rs @@ -655,6 +655,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is synchronized", ""), ("Update client clipboard", ""), ("Untagged", ""), - ("newer-version-of-{}-tip", ""), + ("new-version-of-{}-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 39dda41d99f9..8180b9f5f307 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -655,6 +655,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is synchronized", "A vágólap szinkronizálva van"), ("Update client clipboard", ""), ("Untagged", ""), - ("newer-version-of-{}-tip", ""), + ("new-version-of-{}-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index a23cac7083e4..7d90a3ea4388 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -655,6 +655,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is synchronized", ""), ("Update client clipboard", ""), ("Untagged", ""), - ("newer-version-of-{}-tip", ""), + ("new-version-of-{}-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index 068e03bf49e3..6f5127c10728 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -655,6 +655,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is synchronized", "Gli appunti sono sincronizzati"), ("Update client clipboard", "Aggiorna appunti client"), ("Untagged", "Senza tag"), - ("newer-version-of-{}-tip", ""), + ("new-version-of-{}-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index cb5168725d72..3a967afc6c95 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -655,6 +655,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is synchronized", ""), ("Update client clipboard", ""), ("Untagged", ""), - ("newer-version-of-{}-tip", ""), + ("new-version-of-{}-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index e5d0017a28c3..fa35507f8491 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -655,6 +655,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is synchronized", "클립보드가 동기화됨"), ("Update client clipboard", "클라이언트 클립보드 업데이트"), ("Untagged", "태그 없음"), - ("newer-version-of-{}-tip", ""), + ("new-version-of-{}-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index 4eaf3d767f91..1f88ff773899 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -655,6 +655,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is synchronized", ""), ("Update client clipboard", ""), ("Untagged", ""), - ("newer-version-of-{}-tip", ""), + ("new-version-of-{}-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lt.rs b/src/lang/lt.rs index 45ee545b1ff3..33db01f930ad 100644 --- a/src/lang/lt.rs +++ b/src/lang/lt.rs @@ -655,6 +655,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is synchronized", ""), ("Update client clipboard", ""), ("Untagged", ""), - ("newer-version-of-{}-tip", ""), + ("new-version-of-{}-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lv.rs b/src/lang/lv.rs index f84b2ca51cc5..81c3bedae5f9 100644 --- a/src/lang/lv.rs +++ b/src/lang/lv.rs @@ -655,6 +655,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is synchronized", "Starpliktuve ir sinhronizēta"), ("Update client clipboard", "Atjaunināt klienta starpliktuvi"), ("Untagged", "Neatzīmēts"), - ("newer-version-of-{}-tip", ""), + ("new-version-of-{}-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nb.rs b/src/lang/nb.rs index ccaf9ee9af0c..eb3564b86db2 100644 --- a/src/lang/nb.rs +++ b/src/lang/nb.rs @@ -655,6 +655,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is synchronized", ""), ("Update client clipboard", ""), ("Untagged", ""), - ("newer-version-of-{}-tip", ""), + ("new-version-of-{}-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nl.rs b/src/lang/nl.rs index e1d889e0c19a..e8021993bb06 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -655,6 +655,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is synchronized", "Klembord is gesynchroniseerd"), ("Update client clipboard", "Klembord van client bijwerken"), ("Untagged", ""), - ("newer-version-of-{}-tip", ""), + ("new-version-of-{}-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 5ed4bfb397a6..e28430f498e4 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -655,6 +655,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is synchronized", "Schowek jest zsynchronizowany"), ("Update client clipboard", "Uaktualnij schowek klienta"), ("Untagged", ""), - ("newer-version-of-{}-tip", ""), + ("new-version-of-{}-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index 43cafa6be518..13f829f77afb 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -655,6 +655,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is synchronized", ""), ("Update client clipboard", ""), ("Untagged", ""), - ("newer-version-of-{}-tip", ""), + ("new-version-of-{}-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index c1f96e452e11..eff01dd5eee9 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -655,6 +655,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is synchronized", ""), ("Update client clipboard", ""), ("Untagged", ""), - ("newer-version-of-{}-tip", ""), + ("new-version-of-{}-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ro.rs b/src/lang/ro.rs index 9070b39accab..8bd79c189502 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -655,6 +655,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is synchronized", ""), ("Update client clipboard", ""), ("Untagged", ""), - ("newer-version-of-{}-tip", ""), + ("new-version-of-{}-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 6ce56881352a..2bf6aefef364 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -655,6 +655,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is synchronized", "Буфер обмена синхронизирован"), ("Update client clipboard", "Обновить буфер обмена клиента"), ("Untagged", "Без метки"), - ("newer-version-of-{}-tip", ""), + ("new-version-of-{}-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index bfb1dd1ffd93..96c7977cca2d 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -655,6 +655,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is synchronized", ""), ("Update client clipboard", ""), ("Untagged", ""), - ("newer-version-of-{}-tip", ""), + ("new-version-of-{}-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sl.rs b/src/lang/sl.rs index af40e973f0c1..fad447b692f0 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -655,6 +655,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is synchronized", ""), ("Update client clipboard", ""), ("Untagged", ""), - ("newer-version-of-{}-tip", ""), + ("new-version-of-{}-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sq.rs b/src/lang/sq.rs index aa29aef0d3a1..ad76f2f9cb28 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -655,6 +655,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is synchronized", ""), ("Update client clipboard", ""), ("Untagged", ""), - ("newer-version-of-{}-tip", ""), + ("new-version-of-{}-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sr.rs b/src/lang/sr.rs index 3549c572b497..286658657a06 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -655,6 +655,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is synchronized", ""), ("Update client clipboard", ""), ("Untagged", ""), - ("newer-version-of-{}-tip", ""), + ("new-version-of-{}-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sv.rs b/src/lang/sv.rs index de170cc26114..fcb2fe1ae419 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -655,6 +655,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is synchronized", ""), ("Update client clipboard", ""), ("Untagged", ""), - ("newer-version-of-{}-tip", ""), + ("new-version-of-{}-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index 0240534c872e..9f1293ce1985 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -655,6 +655,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is synchronized", ""), ("Update client clipboard", ""), ("Untagged", ""), - ("newer-version-of-{}-tip", "There is a newer version of {} available"), + ("new-version-of-{}-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/th.rs b/src/lang/th.rs index 4a6107284d1a..83fac2ab8b4f 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -655,6 +655,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is synchronized", ""), ("Update client clipboard", ""), ("Untagged", ""), - ("newer-version-of-{}-tip", ""), + ("new-version-of-{}-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index f7bebb0142aa..630add8bb568 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -655,6 +655,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is synchronized", ""), ("Update client clipboard", ""), ("Untagged", ""), - ("newer-version-of-{}-tip", ""), + ("new-version-of-{}-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index d75e12add769..ea931e2bb15a 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -655,6 +655,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is synchronized", "剪貼簿已同步"), ("Update client clipboard", "更新客戶端的剪貼簿"), ("Untagged", "無標籤"), - ("newer-version-of-{}-tip", ""), + ("new-version-of-{}-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/uk.rs b/src/lang/uk.rs index 805db1afe0a3..148d7d3f653e 100644 --- a/src/lang/uk.rs +++ b/src/lang/uk.rs @@ -655,6 +655,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is synchronized", "Буфер обміну синхронізовано"), ("Update client clipboard", "Оновити буфер обміну клієнта"), ("Untagged", "Без міток"), - ("newer-version-of-{}-tip", ""), + ("new-version-of-{}-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index 81cf265d81cf..1ee2cd6d026e 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -655,6 +655,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is synchronized", ""), ("Update client clipboard", ""), ("Untagged", ""), - ("newer-version-of-{}-tip", ""), + ("new-version-of-{}-tip", ""), ].iter().cloned().collect(); } From 875b738222c75b60da5935e9560eee1ffffdfeed Mon Sep 17 00:00:00 2001 From: Mr-Update <37781396+Mr-Update@users.noreply.github.com> Date: Mon, 9 Dec 2024 05:30:32 +0100 Subject: [PATCH 462/541] Update de.rs (#10228) --- src/lang/de.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lang/de.rs b/src/lang/de.rs index 23ec9247ed80..dbc6efc2d391 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -654,7 +654,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload files", "Dateien hochladen"), ("Clipboard is synchronized", "Zwischenablage ist synchronisiert"), ("Update client clipboard", "Client-Zwischenablage aktualisieren"), - ("Untagged", ""), - ("new-version-of-{}-tip", ""), + ("Untagged", "Unmarkiert"), + ("new-version-of-{}-tip", "Es ist eine neue Version von {} verfügbar"), ].iter().cloned().collect(); } From 0e321bd8457bdefb364273a1483b0540697a93e3 Mon Sep 17 00:00:00 2001 From: XLion Date: Mon, 9 Dec 2024 12:30:50 +0800 Subject: [PATCH 463/541] Update tw.rs (#10231) --- src/lang/tw.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lang/tw.rs b/src/lang/tw.rs index ea931e2bb15a..89854769ea42 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -655,6 +655,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is synchronized", "剪貼簿已同步"), ("Update client clipboard", "更新客戶端的剪貼簿"), ("Untagged", "無標籤"), - ("new-version-of-{}-tip", ""), + ("new-version-of-{}-tip", "有新版本的 {} 可用"), ].iter().cloned().collect(); } From 0f7d78c263b39a3ae128a2994d8c1f3ce0a2d229 Mon Sep 17 00:00:00 2001 From: solokot Date: Mon, 9 Dec 2024 10:11:09 +0300 Subject: [PATCH 464/541] Update ru.rs (#10233) --- src/lang/ru.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 2bf6aefef364..b035947d5614 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -655,6 +655,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is synchronized", "Буфер обмена синхронизирован"), ("Update client clipboard", "Обновить буфер обмена клиента"), ("Untagged", "Без метки"), - ("new-version-of-{}-tip", ""), + ("new-version-of-{}-tip", "Доступна новая версия {}"), ].iter().cloned().collect(); } From a02d2bb4ac030c24a5aa1943b1cfecaf6ae97bd5 Mon Sep 17 00:00:00 2001 From: 21pages Date: Mon, 9 Dec 2024 15:41:49 +0800 Subject: [PATCH 465/541] fix ios audio output (#10235) Signed-off-by: 21pages --- src/client.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/client.rs b/src/client.rs index 9a050c1cb13a..a201336ac0fb 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1002,8 +1002,13 @@ impl AudioHandler { let sample_format = config.sample_format(); log::info!("Default output format: {:?}", config); log::info!("Remote input format: {:?}", format0); + #[allow(unused_mut)] let mut config: StreamConfig = config.into(); - config.buffer_size = cpal::BufferSize::Fixed(64); + #[cfg(not(target_os = "ios"))] + { + // this makes ios audio output not work + config.buffer_size = cpal::BufferSize::Fixed(64); + } self.sample_rate = (format0.sample_rate, config.sample_rate.0); let mut build_output_stream = |config: StreamConfig| match sample_format { From 63e22b7685ecea3dea94cbc85ba863364761eebe Mon Sep 17 00:00:00 2001 From: rustdesk Date: Mon, 9 Dec 2024 18:49:02 +0800 Subject: [PATCH 466/541] fix build error with latest xcode --- flutter/build_ios.sh | 2 ++ flutter/ios/Podfile.lock | 2 +- flutter/ios/Runner/AppDelegate.swift | 2 +- flutter/ios_arm64.sh | 2 ++ flutter/pubspec.lock | 6 +++--- flutter/pubspec.yaml | 1 + 6 files changed, 10 insertions(+), 5 deletions(-) diff --git a/flutter/build_ios.sh b/flutter/build_ios.sh index a6468a0a8fb7..50f2f0056b41 100755 --- a/flutter/build_ios.sh +++ b/flutter/build_ios.sh @@ -2,4 +2,6 @@ # https://docs.flutter.dev/deployment/ios # flutter build ipa --release --obfuscate --split-debug-info=./split-debug-info # no obfuscate, because no easy to check errors +cd $(dirname $(dirname $(which flutter))) +git apply ~/rustdesk/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff flutter build ipa --release diff --git a/flutter/ios/Podfile.lock b/flutter/ios/Podfile.lock index b03cb7466fd8..c9e9f9a2ffdd 100644 --- a/flutter/ios/Podfile.lock +++ b/flutter/ios/Podfile.lock @@ -133,7 +133,7 @@ SPEC CHECKSUMS: sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec SwiftyGif: 93a1cc87bf3a51916001cf8f3d63835fb64c819f uni_links: d97da20c7701486ba192624d99bffaaffcfc298a - url_launcher_ios: bbd758c6e7f9fd7b5b1d4cde34d2b95fcce5e812 + url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe video_player_avfoundation: 02011213dab73ae3687df27ce441fbbcc82b5579 wakelock_plus: 8b09852c8876491e4b6d179e17dfe2a0b5f60d47 diff --git a/flutter/ios/Runner/AppDelegate.swift b/flutter/ios/Runner/AppDelegate.swift index 89e443af6d8b..d9333b7067be 100644 --- a/flutter/ios/Runner/AppDelegate.swift +++ b/flutter/ios/Runner/AppDelegate.swift @@ -1,7 +1,7 @@ import UIKit import Flutter -@UIApplicationMain +@main @objc class AppDelegate: FlutterAppDelegate { override func application( _ application: UIApplication, diff --git a/flutter/ios_arm64.sh b/flutter/ios_arm64.sh index 579baaa6ddae..2d8410c7a4ef 100755 --- a/flutter/ios_arm64.sh +++ b/flutter/ios_arm64.sh @@ -1,2 +1,4 @@ #!/usr/bin/env bash +cd $(dirname $(dirname $(which flutter))) +git apply ~/rustdesk/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff cargo build --features flutter,hwcodec --release --target aarch64-apple-ios --lib diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index 58df59a5521f..8888f9e5734c 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -1364,13 +1364,13 @@ packages: source: hosted version: "6.2.2" url_launcher_ios: - dependency: transitive + dependency: "direct main" description: name: url_launcher_ios - sha256: "75bb6fe3f60070407704282a2d295630cab232991eb52542b18347a8a941df03" + sha256: "16a513b6c12bb419304e72ea0ae2ab4fed569920d1c7cb850263fe3acc824626" url: "https://pub.dev" source: hosted - version: "6.2.4" + version: "6.3.2" url_launcher_linux: dependency: transitive description: diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index 4580af9d3f2e..1776db7a5e71 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -36,6 +36,7 @@ dependencies: #firebase_analytics: ^9.1.5 package_info_plus: ^4.2.0 url_launcher: ^6.2.1 + url_launcher_ios: ^6.3.2 toggle_switch: ^2.1.0 dash_chat_2: git: From d57cf204c82215e0159d7459511392ea6b0be014 Mon Sep 17 00:00:00 2001 From: Alex Rijckaert Date: Tue, 10 Dec 2024 03:48:29 +0100 Subject: [PATCH 467/541] Update nl.rs (#10243) --- src/lang/nl.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lang/nl.rs b/src/lang/nl.rs index e8021993bb06..17bce0695191 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -654,7 +654,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload files", "Bestanden uploaden"), ("Clipboard is synchronized", "Klembord is gesynchroniseerd"), ("Update client clipboard", "Klembord van client bijwerken"), - ("Untagged", ""), - ("new-version-of-{}-tip", ""), + ("Untagged", "Ongemarkeerd"), + ("new-version-of-{}-tip", "Er is een nieuwe versie van {} beschikbaar"), ].iter().cloned().collect(); } From fe06cf77da96793d4ff3ce78c8b4ed2ff1777b6c Mon Sep 17 00:00:00 2001 From: bovirus <1262554+bovirus@users.noreply.github.com> Date: Tue, 10 Dec 2024 03:48:38 +0100 Subject: [PATCH 468/541] Italian language update (#10245) --- src/lang/it.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lang/it.rs b/src/lang/it.rs index 6f5127c10728..46d4c937dc5c 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -655,6 +655,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is synchronized", "Gli appunti sono sincronizzati"), ("Update client clipboard", "Aggiorna appunti client"), ("Untagged", "Senza tag"), - ("new-version-of-{}-tip", ""), + ("new-version-of-{}-tip", "È disponibile una nuova versione di {}"), ].iter().cloned().collect(); } From 0751005073de715abc5758bd58675313ec232e23 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Tue, 10 Dec 2024 11:01:34 +0800 Subject: [PATCH 469/541] Fix/windows empty file clipboard on disconn (#10242) * fix: windows, empty file clipboard on disconn Signed-off-by: fufesou * refact: Don't send files copied before the conn Signed-off-by: fufesou * refact: windows, file clipboard Empty clipboard if no `Ctrl+C` is pressed, but `CliprdrDataObject_GetData()` is called. `CliprdrDataObject_GetData()` is only called in the clipboard object set by RustDesk. Signed-off-by: fufesou --------- Signed-off-by: fufesou --- libs/clipboard/src/windows/wf_cliprdr.c | 65 ++++++++++++++++++++++--- 1 file changed, 59 insertions(+), 6 deletions(-) diff --git a/libs/clipboard/src/windows/wf_cliprdr.c b/libs/clipboard/src/windows/wf_cliprdr.c index c2b7556a46ab..6f8381a6ad40 100644 --- a/libs/clipboard/src/windows/wf_cliprdr.c +++ b/libs/clipboard/src/windows/wf_cliprdr.c @@ -211,6 +211,11 @@ struct wf_clipboard BOOL sync; UINT32 capabilities; + // This flag is not really needed, + // but we can use it to double confirm that files can only be pasted after `Ctrl+C`. + // Not sure `is_file_descriptor_from_remote()` is engough to check all cases on all Windows. + BOOL copied; + size_t map_size; size_t map_capacity; formatMapping *format_mappings; @@ -263,6 +268,8 @@ static UINT cliprdr_send_request_filecontents(wfClipboard *clipboard, UINT32 con ULONG index, UINT32 flag, DWORD positionhigh, DWORD positionlow, ULONG request); +static BOOL is_file_descriptor_from_remote(); + static void CliprdrDataObject_Delete(CliprdrDataObject *instance); static CliprdrEnumFORMATETC *CliprdrEnumFORMATETC_New(ULONG nFormats, FORMATETC *pFormatEtc); @@ -712,6 +719,15 @@ static HRESULT STDMETHODCALLTYPE CliprdrDataObject_GetData(IDataObject *This, FO if (!clipboard) return E_INVALIDARG; + // If `Ctrl+C` is not pressed yet, do not handle the file paste, and empty the clipboard. + if (!clipboard->copied) { + if (try_open_clipboard(clipboard->hwnd)) { + EmptyClipboard(); + CloseClipboard(); + } + return E_UNEXPECTED; + } + if ((idx = cliprdr_lookup_format(instance, pFormatEtc)) == -1) { // empty clipboard here? @@ -1479,6 +1495,8 @@ static UINT cliprdr_send_format_list(wfClipboard *clipboard, UINT32 connID) // send rc = clipboard->context->ClientFormatList(clipboard->context, &formatList); + // No need to check `rc`, `copied` is only used to indicate `Ctrl+C` is pressed. + clipboard->copied = TRUE; for (index = 0; index < numFormats; index++) { @@ -2274,7 +2292,9 @@ static UINT wf_cliprdr_monitor_ready(CliprdrClientContext *context, if (rc != CHANNEL_RC_OK) return rc; - return cliprdr_send_format_list(clipboard, monitorReady->connID); + return rc; + // Don't send format list here, because we don't want to paste files copied before the connection. + // return cliprdr_send_format_list(clipboard, monitorReady->connID); } /** @@ -2321,11 +2341,20 @@ static UINT wf_cliprdr_server_format_list(CliprdrClientContext *context, UINT32 i; formatMapping *mapping; CLIPRDR_FORMAT *format; - wfClipboard *clipboard = (wfClipboard *)context->Custom; + wfClipboard *clipboard = NULL; + + if (!context || !formatList) + return ERROR_INTERNAL_ERROR; + + clipboard = (wfClipboard *)context->Custom; + if (!clipboard) + return ERROR_INTERNAL_ERROR; if (!clear_format_map(clipboard)) return ERROR_INTERNAL_ERROR; + clipboard->copied = TRUE; + for (i = 0; i < formatList->numFormats; i++) { format = &(formatList->formats[i]); @@ -3060,6 +3089,19 @@ wf_cliprdr_server_file_contents_response(CliprdrClientContext *context, return rc; } +BOOL is_file_descriptor_from_remote() +{ + UINT fsid = 0; + if (IsClipboardFormatAvailable(CF_HDROP)) { + return FALSE; + } + fsid = RegisterClipboardFormat(CFSTR_FILEDESCRIPTORW); + if (IsClipboardFormatAvailable(fsid)) { + return TRUE; + } + return FALSE; +} + BOOL wf_cliprdr_init(wfClipboard *clipboard, CliprdrClientContext *cliprdr) { if (!clipboard || !cliprdr) @@ -3071,6 +3113,7 @@ BOOL wf_cliprdr_init(wfClipboard *clipboard, CliprdrClientContext *cliprdr) clipboard->map_size = 0; clipboard->hUser32 = LoadLibraryA("user32.dll"); clipboard->data_obj = NULL; + clipboard->copied = FALSE; if (clipboard->hUser32) { @@ -3126,14 +3169,18 @@ BOOL wf_cliprdr_uninit(wfClipboard *clipboard, CliprdrClientContext *cliprdr) if (!clipboard || !cliprdr) return FALSE; + clipboard->copied = FALSE; cliprdr->Custom = NULL; /* discard all contexts in clipboard */ if (try_open_clipboard(clipboard->hwnd)) { - if (!EmptyClipboard()) + if (is_file_descriptor_from_remote()) { - DEBUG_CLIPRDR("EmptyClipboard failed with 0x%x", GetLastError()); + if (!EmptyClipboard()) + { + DEBUG_CLIPRDR("EmptyClipboard failed with 0x%x", GetLastError()); + } } if (!CloseClipboard()) { @@ -3227,6 +3274,8 @@ BOOL wf_do_empty_cliprdr(wfClipboard *clipboard) return FALSE; } + clipboard->copied = FALSE; + if (WaitForSingleObject(clipboard->data_obj_mutex, INFINITE) != WAIT_OBJECT_0) { return FALSE; @@ -3248,10 +3297,14 @@ BOOL wf_do_empty_cliprdr(wfClipboard *clipboard) break; } - if (!EmptyClipboard()) + if (is_file_descriptor_from_remote()) { - rc = FALSE; + if (!EmptyClipboard()) + { + rc = FALSE; + } } + if (!CloseClipboard()) { // critical error!!! From b24b381575218ba351889b87a82d4cb9135e0070 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Tue, 10 Dec 2024 13:03:00 +0800 Subject: [PATCH 470/541] fix: macos, keyboard, translate mode, capslock and deadkeys (#10248) Signed-off-by: fufesou --- Cargo.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index bc618d760298..4a12e18b1dd8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5219,7 +5219,7 @@ dependencies = [ [[package]] name = "rdev" version = "0.5.0-2" -source = "git+https://github.com/rustdesk-org/rdev#01ac3ec8009f04f7615842b9152338844b806184" +source = "git+https://github.com/rustdesk-org/rdev#f9b60b1dd0f3300a1b797d7a74c116683cd232c8" dependencies = [ "cocoa 0.24.1", "core-foundation 0.9.4", From b0791ba1839c099cdd72de9db9448e46ae75c9f1 Mon Sep 17 00:00:00 2001 From: 21pages Date: Wed, 11 Dec 2024 13:35:50 +0800 Subject: [PATCH 471/541] revert linux use cpal (#10260) Signed-off-by: 21pages --- Cargo.toml | 3 +++ src/client.rs | 57 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index 5949b5925856..7a953f075965 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -78,6 +78,9 @@ fon = "0.6" zip = "0.6" shutdown_hooks = "0.1" totp-rs = { version = "5.4", default-features = false, features = ["gen_secret", "otpauth"] } + +[target.'cfg(not(target_os = "linux"))'.dependencies] +# https://github.com/rustdesk/rustdesk/discussions/10197, not use cpal on linux cpal = { git = "https://github.com/rustdesk-org/cpal", branch = "osx-screencapturekit" } ringbuf = "0.3" diff --git a/src/client.rs b/src/client.rs index a201336ac0fb..2144c23faa35 100644 --- a/src/client.rs +++ b/src/client.rs @@ -2,12 +2,14 @@ use async_trait::async_trait; use bytes::Bytes; #[cfg(not(any(target_os = "android", target_os = "ios")))] use clipboard_master::{CallbackResult, ClipboardHandler}; +#[cfg(not(target_os = "linux"))] use cpal::{ traits::{DeviceTrait, HostTrait, StreamTrait}, Device, Host, StreamConfig, }; use crossbeam_queue::ArrayQueue; use magnum_opus::{Channels::*, Decoder as AudioDecoder}; +#[cfg(not(target_os = "linux"))] use ringbuf::{ring_buffer::RbBase, Rb}; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; @@ -117,6 +119,7 @@ pub const SCRAP_OTHER_VERSION_OR_X11_REQUIRED: &str = pub const SCRAP_X11_REQUIRED: &str = "x11 expected"; pub const SCRAP_X11_REF_URL: &str = "https://rustdesk.com/docs/en/manual/linux/#x11-required"; +#[cfg(not(target_os = "linux"))] pub const AUDIO_BUFFER_MS: usize = 3000; #[cfg(feature = "flutter")] @@ -139,6 +142,7 @@ struct TextClipboardState { running: bool, } +#[cfg(not(target_os = "linux"))] lazy_static::lazy_static! { static ref AUDIO_HOST: Host = cpal::default_host(); } @@ -861,20 +865,28 @@ impl ClipboardHandler for ClientClipboardHandler { #[derive(Default)] pub struct AudioHandler { audio_decoder: Option<(AudioDecoder, Vec)>, + #[cfg(target_os = "linux")] + simple: Option, + #[cfg(not(target_os = "linux"))] audio_buffer: AudioBuffer, sample_rate: (u32, u32), + #[cfg(not(target_os = "linux"))] audio_stream: Option>, channels: u16, + #[cfg(not(target_os = "linux"))] device_channel: u16, + #[cfg(not(target_os = "linux"))] ready: Arc>, } +#[cfg(not(target_os = "linux"))] struct AudioBuffer( pub Arc>>, usize, [usize; 30], ); +#[cfg(not(target_os = "linux"))] impl Default for AudioBuffer { fn default() -> Self { Self( @@ -887,6 +899,7 @@ impl Default for AudioBuffer { } } +#[cfg(not(target_os = "linux"))] impl AudioBuffer { pub fn resize(&mut self, sample_rate: usize, channels: usize) { let capacity = sample_rate * channels * AUDIO_BUFFER_MS / 1000; @@ -989,7 +1002,37 @@ impl AudioBuffer { } impl AudioHandler { + #[cfg(target_os = "linux")] + fn start_audio(&mut self, format0: AudioFormat) -> ResultType<()> { + use psimple::Simple; + use pulse::sample::{Format, Spec}; + use pulse::stream::Direction; + + let spec = Spec { + format: Format::F32le, + channels: format0.channels as _, + rate: format0.sample_rate as _, + }; + if !spec.is_valid() { + bail!("Invalid audio format"); + } + + self.simple = Some(Simple::new( + None, // Use the default server + &crate::get_app_name(), // Our application’s name + Direction::Playback, // We want a playback stream + None, // Use the default device + "playback", // Description of our stream + &spec, // Our sample format + None, // Use default channel map + None, // Use default buffering attributes + )?); + self.sample_rate = (format0.sample_rate, format0.sample_rate); + Ok(()) + } + /// Start the audio playback. + #[cfg(not(target_os = "linux"))] fn start_audio(&mut self, format0: AudioFormat) -> ResultType<()> { let device = AUDIO_HOST .default_output_device() @@ -1057,13 +1100,20 @@ impl AudioHandler { /// Handle audio frame and play it. #[inline] pub fn handle_frame(&mut self, frame: AudioFrame) { + #[cfg(not(target_os = "linux"))] if self.audio_stream.is_none() || !self.ready.lock().unwrap().clone() { return; } + #[cfg(target_os = "linux")] + if self.simple.is_none() { + log::debug!("PulseAudio simple binding does not exists"); + return; + } self.audio_decoder.as_mut().map(|(d, buffer)| { if let Ok(n) = d.decode_float(&frame.data, buffer, false) { let channels = self.channels; let n = n * (channels as usize); + #[cfg(not(target_os = "linux"))] { let sample_rate0 = self.sample_rate.0; let sample_rate = self.sample_rate.1; @@ -1087,11 +1137,18 @@ impl AudioHandler { } self.audio_buffer.append_pcm(&buffer); } + #[cfg(target_os = "linux")] + { + let data_u8 = + unsafe { std::slice::from_raw_parts::(buffer.as_ptr() as _, n * 4) }; + self.simple.as_mut().map(|x| x.write(data_u8)); + } } }); } /// Build audio output stream for current device. + #[cfg(not(target_os = "linux"))] fn build_output_stream>( &mut self, config: &StreamConfig, From 827b5f6a4c0c0208431291177c3d42b271dad835 Mon Sep 17 00:00:00 2001 From: RustDesk <71636191+rustdesk@users.noreply.github.com> Date: Wed, 11 Dec 2024 13:42:25 +0800 Subject: [PATCH 472/541] Revert "revert linux use cpal (#10260)" (#10262) This reverts commit b0791ba1839c099cdd72de9db9448e46ae75c9f1. --- Cargo.toml | 3 --- src/client.rs | 57 --------------------------------------------------- 2 files changed, 60 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 7a953f075965..5949b5925856 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -78,9 +78,6 @@ fon = "0.6" zip = "0.6" shutdown_hooks = "0.1" totp-rs = { version = "5.4", default-features = false, features = ["gen_secret", "otpauth"] } - -[target.'cfg(not(target_os = "linux"))'.dependencies] -# https://github.com/rustdesk/rustdesk/discussions/10197, not use cpal on linux cpal = { git = "https://github.com/rustdesk-org/cpal", branch = "osx-screencapturekit" } ringbuf = "0.3" diff --git a/src/client.rs b/src/client.rs index 2144c23faa35..a201336ac0fb 100644 --- a/src/client.rs +++ b/src/client.rs @@ -2,14 +2,12 @@ use async_trait::async_trait; use bytes::Bytes; #[cfg(not(any(target_os = "android", target_os = "ios")))] use clipboard_master::{CallbackResult, ClipboardHandler}; -#[cfg(not(target_os = "linux"))] use cpal::{ traits::{DeviceTrait, HostTrait, StreamTrait}, Device, Host, StreamConfig, }; use crossbeam_queue::ArrayQueue; use magnum_opus::{Channels::*, Decoder as AudioDecoder}; -#[cfg(not(target_os = "linux"))] use ringbuf::{ring_buffer::RbBase, Rb}; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; @@ -119,7 +117,6 @@ pub const SCRAP_OTHER_VERSION_OR_X11_REQUIRED: &str = pub const SCRAP_X11_REQUIRED: &str = "x11 expected"; pub const SCRAP_X11_REF_URL: &str = "https://rustdesk.com/docs/en/manual/linux/#x11-required"; -#[cfg(not(target_os = "linux"))] pub const AUDIO_BUFFER_MS: usize = 3000; #[cfg(feature = "flutter")] @@ -142,7 +139,6 @@ struct TextClipboardState { running: bool, } -#[cfg(not(target_os = "linux"))] lazy_static::lazy_static! { static ref AUDIO_HOST: Host = cpal::default_host(); } @@ -865,28 +861,20 @@ impl ClipboardHandler for ClientClipboardHandler { #[derive(Default)] pub struct AudioHandler { audio_decoder: Option<(AudioDecoder, Vec)>, - #[cfg(target_os = "linux")] - simple: Option, - #[cfg(not(target_os = "linux"))] audio_buffer: AudioBuffer, sample_rate: (u32, u32), - #[cfg(not(target_os = "linux"))] audio_stream: Option>, channels: u16, - #[cfg(not(target_os = "linux"))] device_channel: u16, - #[cfg(not(target_os = "linux"))] ready: Arc>, } -#[cfg(not(target_os = "linux"))] struct AudioBuffer( pub Arc>>, usize, [usize; 30], ); -#[cfg(not(target_os = "linux"))] impl Default for AudioBuffer { fn default() -> Self { Self( @@ -899,7 +887,6 @@ impl Default for AudioBuffer { } } -#[cfg(not(target_os = "linux"))] impl AudioBuffer { pub fn resize(&mut self, sample_rate: usize, channels: usize) { let capacity = sample_rate * channels * AUDIO_BUFFER_MS / 1000; @@ -1002,37 +989,7 @@ impl AudioBuffer { } impl AudioHandler { - #[cfg(target_os = "linux")] - fn start_audio(&mut self, format0: AudioFormat) -> ResultType<()> { - use psimple::Simple; - use pulse::sample::{Format, Spec}; - use pulse::stream::Direction; - - let spec = Spec { - format: Format::F32le, - channels: format0.channels as _, - rate: format0.sample_rate as _, - }; - if !spec.is_valid() { - bail!("Invalid audio format"); - } - - self.simple = Some(Simple::new( - None, // Use the default server - &crate::get_app_name(), // Our application’s name - Direction::Playback, // We want a playback stream - None, // Use the default device - "playback", // Description of our stream - &spec, // Our sample format - None, // Use default channel map - None, // Use default buffering attributes - )?); - self.sample_rate = (format0.sample_rate, format0.sample_rate); - Ok(()) - } - /// Start the audio playback. - #[cfg(not(target_os = "linux"))] fn start_audio(&mut self, format0: AudioFormat) -> ResultType<()> { let device = AUDIO_HOST .default_output_device() @@ -1100,20 +1057,13 @@ impl AudioHandler { /// Handle audio frame and play it. #[inline] pub fn handle_frame(&mut self, frame: AudioFrame) { - #[cfg(not(target_os = "linux"))] if self.audio_stream.is_none() || !self.ready.lock().unwrap().clone() { return; } - #[cfg(target_os = "linux")] - if self.simple.is_none() { - log::debug!("PulseAudio simple binding does not exists"); - return; - } self.audio_decoder.as_mut().map(|(d, buffer)| { if let Ok(n) = d.decode_float(&frame.data, buffer, false) { let channels = self.channels; let n = n * (channels as usize); - #[cfg(not(target_os = "linux"))] { let sample_rate0 = self.sample_rate.0; let sample_rate = self.sample_rate.1; @@ -1137,18 +1087,11 @@ impl AudioHandler { } self.audio_buffer.append_pcm(&buffer); } - #[cfg(target_os = "linux")] - { - let data_u8 = - unsafe { std::slice::from_raw_parts::(buffer.as_ptr() as _, n * 4) }; - self.simple.as_mut().map(|x| x.write(data_u8)); - } } }); } /// Build audio output stream for current device. - #[cfg(not(target_os = "linux"))] fn build_output_stream>( &mut self, config: &StreamConfig, From 9c4563687531a9d7d4379ab95118ac8eb7b38ec7 Mon Sep 17 00:00:00 2001 From: Yevhen Popok Date: Thu, 12 Dec 2024 05:13:50 +0200 Subject: [PATCH 473/541] Update uk.rs (#10265) --- src/lang/uk.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lang/uk.rs b/src/lang/uk.rs index 148d7d3f653e..9f0dfdefbbc8 100644 --- a/src/lang/uk.rs +++ b/src/lang/uk.rs @@ -364,7 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recording", "Запис"), ("Directory", "Директорія"), ("Automatically record incoming sessions", "Автоматично записувати вхідні сеанси"), - ("Automatically record outgoing sessions", ""), + ("Automatically record outgoing sessions", "Автоматично записувати вихідні сеанси"), ("Change", "Змінити"), ("Start session recording", "Розпочати запис сеансу"), ("Stop session recording", "Закінчити запис сеансу"), @@ -655,6 +655,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is synchronized", "Буфер обміну синхронізовано"), ("Update client clipboard", "Оновити буфер обміну клієнта"), ("Untagged", "Без міток"), - ("new-version-of-{}-tip", ""), + ("new-version-of-{}-tip", "Доступна нова версія {}"), ].iter().cloned().collect(); } From b544a2889b7fe3b6381c13ddfd08f314a613d1d2 Mon Sep 17 00:00:00 2001 From: 21pages Date: Fri, 13 Dec 2024 13:28:48 +0800 Subject: [PATCH 474/541] update vcpkg to 2024.11.16 (#10272) 1. version changes: * vcpkg: 2024.07.12 -> 2024.11.16 * aom (except linux sciter): 3.9.1 -> 3.11.0 * libvpx: 1.14.1 -> 1.15.0 * libyuv: not update because compiled failed on arm64, and didn't apply different version on different archs * opus: already the latest version * ffmpeg: 7.0.2 -> 7.1 2. other changes: * android 5.0 required, otherwise crash when start, because FFmpeg 7.1 link to mediandk directly 3. Tests: * Except arm, arm64, linux amf, ios, all the other codecs are tested * Compile on arm32 linux is not tested, ci is failed before vcpkg install * Tested windows FFmpeg qsv, still no memory leak Signed-off-by: 21pages --- .github/workflows/ci.yml | 4 +- .github/workflows/flutter-build.yml | 6 +- .github/workflows/playground.yml | 4 +- Cargo.lock | 2 +- res/vcpkg/aom/portfile.cmake | 32 +- res/vcpkg/aom/vcpkg.json | 2 +- .../ffmpeg/0001-create-lib-libraries.patch | 27 ++ res/vcpkg/ffmpeg/0004-dependencies.patch | 65 +++ res/vcpkg/ffmpeg/0005-fix-nasm.patch | 133 ++--- res/vcpkg/ffmpeg/0007-fix-lib-naming.patch | 12 + .../ffmpeg/0012-Fix-ssl-110-detection.patch | 14 - .../ffmpeg/0020-fix-aarch64-libswscale.patch | 28 ++ res/vcpkg/ffmpeg/0024-fix-osx-host-c11.patch | 15 + ...av_stream_get_first_dts-for-chromium.patch | 35 ++ ...0041-add-const-for-opengl-definition.patch | 13 + res/vcpkg/ffmpeg/0042-fix-arm64-linux.patch | 9 + res/vcpkg/ffmpeg/0043-fix-miss-head.patch | 12 + ...dd-query_timeout-option-for-h264-hev.patch | 28 +- ...-amfenc-reconfig-when-bitrate-change.patch | 16 +- .../ffmpeg/patch/0003-amf-colorspace.patch | 161 ------ .../0004-videotoolbox-changing-bitrate.patch | 27 +- .../0005-mediacodec-changing-bitrate.patch | 34 +- .../ffmpeg/patch/0006-dlopen-libva.patch | 458 +++++++----------- .../patch/0007-fix-linux-configure.patch | 30 ++ res/vcpkg/ffmpeg/portfile.cmake | 18 +- res/vcpkg/ffmpeg/vcpkg.json | 4 +- res/vcpkg/libvpx/portfile.cmake | 2 +- res/vcpkg/libvpx/vcpkg.json | 2 +- vcpkg.json | 2 +- 29 files changed, 601 insertions(+), 594 deletions(-) create mode 100644 res/vcpkg/ffmpeg/0001-create-lib-libraries.patch create mode 100644 res/vcpkg/ffmpeg/0004-dependencies.patch create mode 100644 res/vcpkg/ffmpeg/0007-fix-lib-naming.patch delete mode 100644 res/vcpkg/ffmpeg/0012-Fix-ssl-110-detection.patch create mode 100644 res/vcpkg/ffmpeg/0020-fix-aarch64-libswscale.patch create mode 100644 res/vcpkg/ffmpeg/0024-fix-osx-host-c11.patch create mode 100644 res/vcpkg/ffmpeg/0040-ffmpeg-add-av_stream_get_first_dts-for-chromium.patch create mode 100644 res/vcpkg/ffmpeg/0041-add-const-for-opengl-definition.patch create mode 100644 res/vcpkg/ffmpeg/0042-fix-arm64-linux.patch create mode 100644 res/vcpkg/ffmpeg/0043-fix-miss-head.patch delete mode 100644 res/vcpkg/ffmpeg/patch/0003-amf-colorspace.patch create mode 100644 res/vcpkg/ffmpeg/patch/0007-fix-linux-configure.patch diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 90f312968e07..0f9ae95d57b1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,9 +4,9 @@ env: # MIN_SUPPORTED_RUST_VERSION: "1.46.0" # CICD_INTERMEDIATES_DIR: "_cicd-intermediates" VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite" - # vcpkg version: 2024.06.15 + # vcpkg version: 2024.11.16 # for multiarch gcc compatibility - VCPKG_COMMIT_ID: "f7423ee180c4b7f40d43402c2feb3859161ef625" + VCPKG_COMMIT_ID: "b2cb0da531c2f1f740045bfe7c4dac59f0b2b69c" on: workflow_dispatch: diff --git a/.github/workflows/flutter-build.yml b/.github/workflows/flutter-build.yml index 9ec7d9e012d4..1b6dbf1cdc99 100644 --- a/.github/workflows/flutter-build.yml +++ b/.github/workflows/flutter-build.yml @@ -31,8 +31,8 @@ env: FLUTTER_ELINUX_VERSION: "3.16.9" TAG_NAME: "${{ inputs.upload-tag }}" VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite" - # vcpkg version: 2024.07.12 - VCPKG_COMMIT_ID: "1de2026f28ead93ff1773e6e680387643e914ea1" + # vcpkg version: 2024.11.16 + VCPKG_COMMIT_ID: "b2cb0da531c2f1f740045bfe7c4dac59f0b2b69c" VERSION: "1.3.5" NDK_VERSION: "r27c" #signing keys env variable checks @@ -1852,6 +1852,8 @@ jobs: cat ~/.cargo/config # install dependencies from vcpkg export VCPKG_ROOT=/opt/artifacts/vcpkg + # remove this when support higher version + export USE_AOM_391=1 if ! $VCPKG_ROOT/vcpkg install --triplet ${{ matrix.job.vcpkg-triplet }} --x-install-root="$VCPKG_ROOT/installed"; then find "${VCPKG_ROOT}/" -name "*.log" | while read -r _1; do echo "$_1:" diff --git a/.github/workflows/playground.yml b/.github/workflows/playground.yml index d60d6f7b1370..bf7dcd19ecff 100644 --- a/.github/workflows/playground.yml +++ b/.github/workflows/playground.yml @@ -16,8 +16,8 @@ env: 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" + # vcpkg version: 2024.11.16 + VCPKG_COMMIT_ID: "b2cb0da531c2f1f740045bfe7c4dac59f0b2b69c" VERSION: "1.3.5" NDK_VERSION: "r26d" #signing keys env variable checks diff --git a/Cargo.lock b/Cargo.lock index 4a12e18b1dd8..61e6767a34f7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3065,7 +3065,7 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hwcodec" version = "0.7.1" -source = "git+https://github.com/rustdesk-org/hwcodec#835e599ed229e4e01b6fa3566e02ea45c73e2e9c" +source = "git+https://github.com/rustdesk-org/hwcodec#7ee119a58b6ee6ca255a438af69ad0785ba44797" dependencies = [ "bindgen 0.59.2", "cc", diff --git a/res/vcpkg/aom/portfile.cmake b/res/vcpkg/aom/portfile.cmake index 2df452a640e7..24b025173486 100644 --- a/res/vcpkg/aom/portfile.cmake +++ b/res/vcpkg/aom/portfile.cmake @@ -8,16 +8,28 @@ vcpkg_find_acquire_program(PERL) get_filename_component(PERL_PATH ${PERL} DIRECTORY) vcpkg_add_to_path(${PERL_PATH}) -vcpkg_from_git( - OUT_SOURCE_PATH SOURCE_PATH - URL "https://aomedia.googlesource.com/aom" - REF 8ad484f8a18ed1853c094e7d3a4e023b2a92df28 # 3.9.1 - PATCHES - aom-uninitialized-pointer.diff - aom-avx2.diff - # Can be dropped when https://bugs.chromium.org/p/aomedia/issues/detail?id=3029 is merged into the upstream - aom-install.diff -) +if(DEFINED ENV{USE_AOM_391}) + vcpkg_from_git( + OUT_SOURCE_PATH SOURCE_PATH + URL "https://aomedia.googlesource.com/aom" + REF 8ad484f8a18ed1853c094e7d3a4e023b2a92df28 # 3.9.1 + PATCHES + aom-uninitialized-pointer.diff + aom-avx2.diff + aom-install.diff + ) +else() + vcpkg_from_git( + OUT_SOURCE_PATH SOURCE_PATH + URL "https://aomedia.googlesource.com/aom" + REF d6f30ae474dd6c358f26de0a0fc26a0d7340a84c # 3.11.0 + PATCHES + aom-uninitialized-pointer.diff + # aom-avx2.diff + # Can be dropped when https://bugs.chromium.org/p/aomedia/issues/detail?id=3029 is merged into the upstream + aom-install.diff + ) +endif() set(aom_target_cpu "") if(VCPKG_TARGET_IS_UWP OR (VCPKG_TARGET_IS_WINDOWS AND VCPKG_TARGET_ARCHITECTURE MATCHES "^arm")) diff --git a/res/vcpkg/aom/vcpkg.json b/res/vcpkg/aom/vcpkg.json index 78ccc8989099..9ff755f6be6e 100644 --- a/res/vcpkg/aom/vcpkg.json +++ b/res/vcpkg/aom/vcpkg.json @@ -1,6 +1,6 @@ { "name": "aom", - "version-semver": "3.9.1", + "version-semver": "3.11.0", "port-version": 0, "description": "AV1 codec library", "homepage": "https://aomedia.googlesource.com/aom", diff --git a/res/vcpkg/ffmpeg/0001-create-lib-libraries.patch b/res/vcpkg/ffmpeg/0001-create-lib-libraries.patch new file mode 100644 index 000000000000..ced7ba86be2b --- /dev/null +++ b/res/vcpkg/ffmpeg/0001-create-lib-libraries.patch @@ -0,0 +1,27 @@ +diff --git a/configure b/configure +index 1f0b9497cb..3243e23021 100644 +--- a/configure ++++ b/configure +@@ -5697,17 +5697,19 @@ case $target_os in + ;; + win32|win64) + disable symver +- if enabled shared; then ++# if enabled shared; then + # Link to the import library instead of the normal static library + # for shared libs. + LD_LIB='%.lib' + # Cannot build both shared and static libs with MSVC or icl. +- disable static +- fi ++# disable static ++# fi + ! enabled small && test_cmd $windres --version && enable gnu_windres + enabled x86_32 && check_ldflags -LARGEADDRESSAWARE + add_cppflags -DWIN32_LEAN_AND_MEAN + shlibdir_default="$bindir_default" ++ LIBPREF="" ++ LIBSUF=".lib" + SLIBPREF="" + SLIBSUF=".dll" + SLIBNAME_WITH_VERSION='$(SLIBPREF)$(FULLNAME)-$(LIBVERSION)$(SLIBSUF)' diff --git a/res/vcpkg/ffmpeg/0004-dependencies.patch b/res/vcpkg/ffmpeg/0004-dependencies.patch new file mode 100644 index 000000000000..f1f6e72bee3f --- /dev/null +++ b/res/vcpkg/ffmpeg/0004-dependencies.patch @@ -0,0 +1,65 @@ +diff --git a/configure b/configure +index a8b74e0..c99f41c 100755 +--- a/configure ++++ b/configure +@@ -6633,7 +6633,7 @@ fi + + enabled zlib && { check_pkg_config zlib zlib "zlib.h" zlibVersion || + check_lib zlib zlib.h zlibVersion -lz; } +-enabled bzlib && check_lib bzlib bzlib.h BZ2_bzlibVersion -lbz2 ++enabled bzlib && require_pkg_config bzlib bzip2 bzlib.h BZ2_bzlibVersion + enabled lzma && check_lib lzma lzma.h lzma_version_number -llzma + + enabled zlib && test_exec $zlib_extralibs <= 3.98.3" lame/lame.h lame_set_VBR_quality -lmp3lame $libm_extralibs ++enabled libmp3lame && { check_lib libmp3lame lame/lame.h lame_set_VBR_quality -lmp3lame $libm_extralibs || ++ require libmp3lame lame/lame.h lame_set_VBR_quality -llibmp3lame-static -llibmpghip-static $libm_extralibs; } + enabled libmysofa && { check_pkg_config libmysofa libmysofa mysofa.h mysofa_neighborhood_init_withstepdefine || + require libmysofa mysofa.h mysofa_neighborhood_init_withstepdefine -lmysofa $zlib_extralibs; } + enabled libnpp && { check_lib libnpp npp.h nppGetLibVersion -lnppig -lnppicc -lnppc -lnppidei -lnppif || +@@ -6772,7 +6773,7 @@ require_pkg_config libopencv opencv opencv/cxcore.h cvCreateImageHeader; } + enabled libopenh264 && require_pkg_config libopenh264 "openh264 >= 1.3.0" wels/codec_api.h WelsGetCodecVersion + enabled libopenjpeg && { check_pkg_config libopenjpeg "libopenjp2 >= 2.1.0" openjpeg.h opj_version || + { require_pkg_config libopenjpeg "libopenjp2 >= 2.1.0" openjpeg.h opj_version -DOPJ_STATIC && add_cppflags -DOPJ_STATIC; } } +-enabled libopenmpt && require_pkg_config libopenmpt "libopenmpt >= 0.2.6557" libopenmpt/libopenmpt.h openmpt_module_create -lstdc++ && append libopenmpt_extralibs "-lstdc++" ++enabled libopenmpt && require_pkg_config libopenmpt "libopenmpt >= 0.2.6557" libopenmpt/libopenmpt.h openmpt_module_create + enabled libopenvino && { { check_pkg_config libopenvino openvino openvino/c/openvino.h ov_core_create && enable openvino2; } || + { check_pkg_config libopenvino openvino c_api/ie_c_api.h ie_c_api_version || + require libopenvino c_api/ie_c_api.h ie_c_api_version -linference_engine_c_api; } } +@@ -6796,8 +6797,8 @@ enabled libshaderc && require_pkg_config spirv_compiler "shaderc >= 2019. + enabled libshine && require_pkg_config libshine shine shine/layer3.h shine_encode_buffer + enabled libsmbclient && { check_pkg_config libsmbclient smbclient libsmbclient.h smbc_init || + require libsmbclient libsmbclient.h smbc_init -lsmbclient; } +-enabled libsnappy && require libsnappy snappy-c.h snappy_compress -lsnappy -lstdc++ +-enabled libsoxr && require libsoxr soxr.h soxr_create -lsoxr ++enabled libsnappy && require_pkg_config libsnappy snappy snappy-c.h snappy_compress ++enabled libsoxr && require libsoxr soxr.h soxr_create -lsoxr $libm_extralibs + enabled libssh && require_pkg_config libssh "libssh >= 0.6.0" libssh/sftp.h sftp_init + enabled libspeex && require_pkg_config libspeex speex speex/speex.h speex_decoder_init + enabled libsrt && require_pkg_config libsrt "srt >= 1.3.0" srt/srt.h srt_socket +@@ -6880,6 +6881,8 @@ enabled openal && { check_pkg_config openal "openal >= 1.1" "AL/al.h" + enabled opencl && { check_pkg_config opencl OpenCL CL/cl.h clEnqueueNDRangeKernel || + check_lib opencl OpenCL/cl.h clEnqueueNDRangeKernel "-framework OpenCL" || + check_lib opencl CL/cl.h clEnqueueNDRangeKernel -lOpenCL || ++ check_lib opencl CL/cl.h clEnqueueNDRangeKernel -lOpenCL -lAdvapi32 -lOle32 -lCfgmgr32|| ++ check_lib opencl CL/cl.h clEnqueueNDRangeKernel -lOpenCL -pthread -ldl || + die "ERROR: opencl not found"; } && + { test_cpp_condition "OpenCL/cl.h" "defined(CL_VERSION_1_2)" || + test_cpp_condition "CL/cl.h" "defined(CL_VERSION_1_2)" || +@@ -7204,10 +7207,10 @@ enabled amf && + "(AMF_VERSION_MAJOR << 48 | AMF_VERSION_MINOR << 32 | AMF_VERSION_RELEASE << 16 | AMF_VERSION_BUILD_NUM) >= 0x0001000400210000" + + # Funny iconv installations are not unusual, so check it after all flags have been set +-if enabled libc_iconv; then ++if enabled libc_iconv && disabled iconv; then + check_func_headers iconv.h iconv + elif enabled iconv; then +- check_func_headers iconv.h iconv || check_lib iconv iconv.h iconv -liconv ++ check_func_headers iconv.h iconv || check_lib iconv iconv.h iconv -liconv || check_lib iconv iconv.h iconv -liconv -lcharset + fi + + enabled debug && add_cflags -g"$debuglevel" && add_asflags -g"$debuglevel" diff --git a/res/vcpkg/ffmpeg/0005-fix-nasm.patch b/res/vcpkg/ffmpeg/0005-fix-nasm.patch index 9308e714a6bb..68b7503b2442 100644 --- a/res/vcpkg/ffmpeg/0005-fix-nasm.patch +++ b/res/vcpkg/ffmpeg/0005-fix-nasm.patch @@ -1,55 +1,78 @@ -diff --git a/libavcodec/x86/Makefile b/libavcodec/x86/Makefile ---- a/libavcodec/x86/Makefile -+++ b/libavcodec/x86/Makefile -@@ -158,6 +158,8 @@ X86ASM-OBJS-$(CONFIG_ALAC_DECODER) += x86/alacdsp.o - X86ASM-OBJS-$(CONFIG_APNG_DECODER) += x86/pngdsp.o - X86ASM-OBJS-$(CONFIG_CAVS_DECODER) += x86/cavsidct.o -+ifdef ARCH_X86_64 - X86ASM-OBJS-$(CONFIG_CFHD_ENCODER) += x86/cfhdencdsp.o -+endif - X86ASM-OBJS-$(CONFIG_CFHD_DECODER) += x86/cfhddsp.o - X86ASM-OBJS-$(CONFIG_DCA_DECODER) += x86/dcadsp.o x86/synth_filter.o - X86ASM-OBJS-$(CONFIG_DIRAC_DECODER) += x86/diracdsp.o \ -@@ -175,15 +177,21 @@ x86/hevc_sao_10bit.o - X86ASM-OBJS-$(CONFIG_JPEG2000_DECODER) += x86/jpeg2000dsp.o - X86ASM-OBJS-$(CONFIG_LSCR_DECODER) += x86/pngdsp.o -+ifdef ARCH_X86_64 - X86ASM-OBJS-$(CONFIG_MLP_DECODER) += x86/mlpdsp.o -+endif - X86ASM-OBJS-$(CONFIG_MPEG4_DECODER) += x86/xvididct.o - X86ASM-OBJS-$(CONFIG_PNG_DECODER) += x86/pngdsp.o -+ifdef ARCH_X86_64 - X86ASM-OBJS-$(CONFIG_PRORES_DECODER) += x86/proresdsp.o - X86ASM-OBJS-$(CONFIG_PRORES_LGPL_DECODER) += x86/proresdsp.o -+endif - X86ASM-OBJS-$(CONFIG_RV40_DECODER) += x86/rv40dsp.o - X86ASM-OBJS-$(CONFIG_SBC_ENCODER) += x86/sbcdsp.o - X86ASM-OBJS-$(CONFIG_SVQ1_ENCODER) += x86/svq1enc.o - X86ASM-OBJS-$(CONFIG_TAK_DECODER) += x86/takdsp.o -+ifdef ARCH_X86_64 - X86ASM-OBJS-$(CONFIG_TRUEHD_DECODER) += x86/mlpdsp.o -+endif - X86ASM-OBJS-$(CONFIG_TTA_DECODER) += x86/ttadsp.o - X86ASM-OBJS-$(CONFIG_TTA_ENCODER) += x86/ttaencdsp.o - X86ASM-OBJS-$(CONFIG_UTVIDEO_DECODER) += x86/utvideodsp.o -diff --git a/libavfilter/x86/Makefile b/libavfilter/x86/Makefile ---- a/libavfilter/x86/Makefile -+++ b/libavfilter/x86/Makefile -@@ -44,6 +44,8 @@ - X86ASM-OBJS-$(CONFIG_AFIR_FILTER) += x86/af_afir.o - X86ASM-OBJS-$(CONFIG_ANLMDN_FILTER) += x86/af_anlmdn.o -+ifdef ARCH_X86_64 - X86ASM-OBJS-$(CONFIG_ATADENOISE_FILTER) += x86/vf_atadenoise.o -+endif - X86ASM-OBJS-$(CONFIG_BLEND_FILTER) += x86/vf_blend.o - X86ASM-OBJS-$(CONFIG_BWDIF_FILTER) += x86/vf_bwdif.o - X86ASM-OBJS-$(CONFIG_COLORSPACE_FILTER) += x86/colorspacedsp.o -@@ -62,6 +62,8 @@ X86ASM-OBJS-$(CONFIG_LUT3D_FILTER) += x86/vf_lut3d.o - X86ASM-OBJS-$(CONFIG_MASKEDCLAMP_FILTER) += x86/vf_maskedclamp.o - X86ASM-OBJS-$(CONFIG_MASKEDMERGE_FILTER) += x86/vf_maskedmerge.o -+ifdef ARCH_X86_64 - X86ASM-OBJS-$(CONFIG_NLMEANS_FILTER) += x86/vf_nlmeans.o -+endif - X86ASM-OBJS-$(CONFIG_OVERLAY_FILTER) += x86/vf_overlay.o - X86ASM-OBJS-$(CONFIG_PP7_FILTER) += x86/vf_pp7.o - X86ASM-OBJS-$(CONFIG_PSNR_FILTER) += x86/vf_psnr.o +diff --git a/libavcodec/x86/mlpdsp.asm b/libavcodec/x86/mlpdsp.asm +index 3dc641e..609b834 100644 +--- a/libavcodec/x86/mlpdsp.asm ++++ b/libavcodec/x86/mlpdsp.asm +@@ -23,7 +23,9 @@ + + SECTION .text + +-%if ARCH_X86_64 ++%ifn ARCH_X86_64 ++mlpdsp_placeholder: times 4 db 0 ++%else + + %macro SHLX 2 + %if cpuflag(bmi2) +diff --git a/libavcodec/x86/proresdsp.asm b/libavcodec/x86/proresdsp.asm +index 65c9fad..5ad73f3 100644 +--- a/libavcodec/x86/proresdsp.asm ++++ b/libavcodec/x86/proresdsp.asm +@@ -24,7 +24,10 @@ + + %include "libavutil/x86/x86util.asm" + +-%if ARCH_X86_64 ++%ifn ARCH_X86_64 ++SECTION .rdata ++proresdsp_placeholder: times 4 db 0 ++%else + + SECTION_RODATA + +diff --git a/libavcodec/x86/vvc/vvc_mc.asm b/libavcodec/x86/vvc/vvc_mc.asm +index 30aa97c..3975f98 100644 +--- a/libavcodec/x86/vvc/vvc_mc.asm ++++ b/libavcodec/x86/vvc/vvc_mc.asm +@@ -31,7 +31,9 @@ + + SECTION_RODATA 32 + +-%if ARCH_X86_64 ++%ifn ARCH_X86_64 ++vvc_mc_placeholder: times 4 db 0 ++%else + + %if HAVE_AVX2_EXTERNAL + +diff --git a/libavfilter/x86/vf_atadenoise.asm b/libavfilter/x86/vf_atadenoise.asm +index 4945ad3..748b65a 100644 +--- a/libavfilter/x86/vf_atadenoise.asm ++++ b/libavfilter/x86/vf_atadenoise.asm +@@ -20,7 +20,10 @@ + ;* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + ;****************************************************************************** + +-%if ARCH_X86_64 ++%ifn ARCH_X86_64 ++SECTION .rdata ++vf_atadenoise_placeholder: times 4 db 0 ++%else + + %include "libavutil/x86/x86util.asm" + +diff --git a/libavfilter/x86/vf_nlmeans.asm b/libavfilter/x86/vf_nlmeans.asm +index 8f57801..9aef3a4 100644 +--- a/libavfilter/x86/vf_nlmeans.asm ++++ b/libavfilter/x86/vf_nlmeans.asm +@@ -21,7 +21,10 @@ + + %include "libavutil/x86/x86util.asm" + +-%if HAVE_AVX2_EXTERNAL && ARCH_X86_64 ++%ifn HAVE_AVX2_EXTERNAL && ARCH_X86_64 ++SECTION .rdata ++vf_nlmeans_placeholder: times 4 db 0 ++%else + + SECTION_RODATA 32 + diff --git a/res/vcpkg/ffmpeg/0007-fix-lib-naming.patch b/res/vcpkg/ffmpeg/0007-fix-lib-naming.patch new file mode 100644 index 000000000000..c22f9c1999d0 --- /dev/null +++ b/res/vcpkg/ffmpeg/0007-fix-lib-naming.patch @@ -0,0 +1,12 @@ +diff --git a/configure b/configure +index d6c4388..75b96c3 100644 +--- a/configure ++++ b/configure +@@ -4781,6 +4781,7 @@ msvc_common_flags(){ + -mfp16-format=*) ;; + -lz) echo zlib.lib ;; + -lx264) echo libx264.lib ;; ++ -lmp3lame) echo libmp3lame.lib ;; + -lstdc++) ;; + -l*) echo ${flag#-l}.lib ;; + -LARGEADDRESSAWARE) echo $flag ;; diff --git a/res/vcpkg/ffmpeg/0012-Fix-ssl-110-detection.patch b/res/vcpkg/ffmpeg/0012-Fix-ssl-110-detection.patch deleted file mode 100644 index b2e5501a13a3..000000000000 --- a/res/vcpkg/ffmpeg/0012-Fix-ssl-110-detection.patch +++ /dev/null @@ -1,14 +0,0 @@ -diff --git a/configure b/configure -index 2be953f7e7..e075949ffc 100755 ---- a/configure -+++ b/configure -@@ -6497,6 +6497,7 @@ enabled openssl && { { check_pkg_config openssl "openssl >= 3.0.0 - { enabled gplv3 || ! enabled gpl || enabled nonfree || die "ERROR: OpenSSL >=3.0.0 requires --enable-version3"; }; } || - { enabled gpl && ! enabled nonfree && die "ERROR: OpenSSL <3.0.0 is incompatible with the gpl"; } || - check_pkg_config openssl openssl openssl/ssl.h OPENSSL_init_ssl || - check_pkg_config openssl openssl openssl/ssl.h SSL_library_init || -+ check_lib openssl openssl/ssl.h OPENSSL_init_ssl -lssl -lcrypto $pthreads_extralibs -ldl || - check_lib openssl openssl/ssl.h OPENSSL_init_ssl -lssl -lcrypto || - check_lib openssl openssl/ssl.h SSL_library_init -lssl -lcrypto || - check_lib openssl openssl/ssl.h SSL_library_init -lssl32 -leay32 || - diff --git a/res/vcpkg/ffmpeg/0020-fix-aarch64-libswscale.patch b/res/vcpkg/ffmpeg/0020-fix-aarch64-libswscale.patch new file mode 100644 index 000000000000..f47e82ed8a20 --- /dev/null +++ b/res/vcpkg/ffmpeg/0020-fix-aarch64-libswscale.patch @@ -0,0 +1,28 @@ +diff --git a/libswscale/aarch64/yuv2rgb_neon.S b/libswscale/aarch64/yuv2rgb_neon.S +index 89d69e7f6c..4bc1607a7a 100644 +--- a/libswscale/aarch64/yuv2rgb_neon.S ++++ b/libswscale/aarch64/yuv2rgb_neon.S +@@ -169,19 +169,19 @@ function ff_\ifmt\()_to_\ofmt\()_neon, export=1 + sqdmulh v26.8h, v26.8h, v0.8h // ((Y1*(1<<3) - y_offset) * y_coeff) >> 15 + sqdmulh v27.8h, v27.8h, v0.8h // ((Y2*(1<<3) - y_offset) * y_coeff) >> 15 + +-.ifc \ofmt,argb // 1 2 3 0 ++.ifc \ofmt,argb + compute_rgba v5.8b,v6.8b,v7.8b,v4.8b, v17.8b,v18.8b,v19.8b,v16.8b + .endif + +-.ifc \ofmt,rgba // 0 1 2 3 ++.ifc \ofmt,rgba + compute_rgba v4.8b,v5.8b,v6.8b,v7.8b, v16.8b,v17.8b,v18.8b,v19.8b + .endif + +-.ifc \ofmt,abgr // 3 2 1 0 ++.ifc \ofmt,abgr + compute_rgba v7.8b,v6.8b,v5.8b,v4.8b, v19.8b,v18.8b,v17.8b,v16.8b + .endif + +-.ifc \ofmt,bgra // 2 1 0 3 ++.ifc \ofmt,bgra + compute_rgba v6.8b,v5.8b,v4.8b,v7.8b, v18.8b,v17.8b,v16.8b,v19.8b + .endif + diff --git a/res/vcpkg/ffmpeg/0024-fix-osx-host-c11.patch b/res/vcpkg/ffmpeg/0024-fix-osx-host-c11.patch new file mode 100644 index 000000000000..dbce2f53b8e7 --- /dev/null +++ b/res/vcpkg/ffmpeg/0024-fix-osx-host-c11.patch @@ -0,0 +1,15 @@ +diff --git a/configure b/configure +index 4f5353f84b..dd9147c677 100755 +--- a/configure ++++ b/configure +@@ -5607,8 +5607,8 @@ check_cppflags -D_FILE_OFFSET_BITS=64 + check_cppflags -D_LARGEFILE_SOURCE + + add_host_cppflags -D_ISOC11_SOURCE + check_host_cflags_cc -std=$stdc ctype.h "__STDC_VERSION__ >= 201112L" || +- check_host_cflags_cc -std=c11 ctype.h "__STDC_VERSION__ >= 201112L" || die "Host compiler lacks C11 support" ++ check_host_cflags_cc -std=c11 ctype.h "__STDC_VERSION__ >= 201112L" + + check_host_cflags -Wall + check_host_cflags $host_cflags_speed + diff --git a/res/vcpkg/ffmpeg/0040-ffmpeg-add-av_stream_get_first_dts-for-chromium.patch b/res/vcpkg/ffmpeg/0040-ffmpeg-add-av_stream_get_first_dts-for-chromium.patch new file mode 100644 index 000000000000..c2e1d8ff0d7b --- /dev/null +++ b/res/vcpkg/ffmpeg/0040-ffmpeg-add-av_stream_get_first_dts-for-chromium.patch @@ -0,0 +1,35 @@ +diff --git a/libavformat/avformat.h b/libavformat/avformat.h +index cd7b0d941c..b4a6dce885 100644 +--- a/libavformat/avformat.h ++++ b/libavformat/avformat.h +@@ -1169,7 +1169,11 @@ typedef struct AVStreamGroup { + } AVStreamGroup; + + struct AVCodecParserContext *av_stream_get_parser(const AVStream *s); + ++// Chromium: We use the internal field first_dts vvv ++int64_t av_stream_get_first_dts(const AVStream *st); ++// Chromium: We use the internal field first_dts ^^^ ++ + #define AV_PROGRAM_RUNNING 1 + + /** +diff --git a/libavformat/mux_utils.c b/libavformat/mux_utils.c +index de7580c32d..0ef0fe530e 100644 +--- a/libavformat/mux_utils.c ++++ b/libavformat/mux_utils.c +@@ -29,7 +29,14 @@ #include "avformat.h" + #include "avio.h" + #include "internal.h" + #include "mux.h" + ++// Chromium: We use the internal field first_dts vvv ++int64_t av_stream_get_first_dts(const AVStream *st) ++{ ++ return cffstream(st)->first_dts; ++} ++// Chromium: We use the internal field first_dts ^^^ ++ + int avformat_query_codec(const AVOutputFormat *ofmt, enum AVCodecID codec_id, + int std_compliance) + { diff --git a/res/vcpkg/ffmpeg/0041-add-const-for-opengl-definition.patch b/res/vcpkg/ffmpeg/0041-add-const-for-opengl-definition.patch new file mode 100644 index 000000000000..b22b40d1f377 --- /dev/null +++ b/res/vcpkg/ffmpeg/0041-add-const-for-opengl-definition.patch @@ -0,0 +1,13 @@ +diff --git a/libavdevice/opengl_enc.c b/libavdevice/opengl_enc.c +index b2ac6eb..6351614 100644 +--- a/libavdevice/opengl_enc.c ++++ b/libavdevice/opengl_enc.c +@@ -116,7 +116,7 @@ typedef void (APIENTRY *FF_PFNGLATTACHSHADERPROC) (GLuint program, GLuint shad + typedef GLuint (APIENTRY *FF_PFNGLCREATESHADERPROC) (GLenum type); + typedef void (APIENTRY *FF_PFNGLDELETESHADERPROC) (GLuint shader); + typedef void (APIENTRY *FF_PFNGLCOMPILESHADERPROC) (GLuint shader); +-typedef void (APIENTRY *FF_PFNGLSHADERSOURCEPROC) (GLuint shader, GLsizei count, const char* *string, const GLint *length); ++typedef void (APIENTRY *FF_PFNGLSHADERSOURCEPROC) (GLuint shader, GLsizei count, const char* const *string, const GLint *length); + typedef void (APIENTRY *FF_PFNGLGETSHADERIVPROC) (GLuint shader, GLenum pname, GLint *params); + typedef void (APIENTRY *FF_PFNGLGETSHADERINFOLOGPROC) (GLuint shader, GLsizei bufSize, GLsizei *length, char *infoLog); + diff --git a/res/vcpkg/ffmpeg/0042-fix-arm64-linux.patch b/res/vcpkg/ffmpeg/0042-fix-arm64-linux.patch new file mode 100644 index 000000000000..6ff63c3718d6 --- /dev/null +++ b/res/vcpkg/ffmpeg/0042-fix-arm64-linux.patch @@ -0,0 +1,9 @@ +diff --git a/ffbuild/libversion.sh b/ffbuild/libversion.sh +index a94ab58..ecaa90c 100644 +--- a/ffbuild/libversion.sh ++++ b/ffbuild/libversion.sh +@@ -1,3 +1,4 @@ ++#!/bin/sh + toupper(){ + echo "$@" | tr abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ + } diff --git a/res/vcpkg/ffmpeg/0043-fix-miss-head.patch b/res/vcpkg/ffmpeg/0043-fix-miss-head.patch new file mode 100644 index 000000000000..bad42798c83e --- /dev/null +++ b/res/vcpkg/ffmpeg/0043-fix-miss-head.patch @@ -0,0 +1,12 @@ +diff --git a/libavfilter/textutils.c b/libavfilter/textutils.c +index ef658d0..c61b0ad 100644 +--- a/libavfilter/textutils.c ++++ b/libavfilter/textutils.c +@@ -31,6 +31,7 @@ + #include "libavutil/file.h" + #include "libavutil/mem.h" + #include "libavutil/time.h" ++#include "libavutil/time_internal.h" + + static int ff_expand_text_function_internal(FFExpandTextContext *expand_text, AVBPrint *bp, + char *name, unsigned argc, char **argv) diff --git a/res/vcpkg/ffmpeg/patch/0001-avcodec-amfenc-add-query_timeout-option-for-h264-hev.patch b/res/vcpkg/ffmpeg/patch/0001-avcodec-amfenc-add-query_timeout-option-for-h264-hev.patch index 5431b3edd05f..4fbce0d48494 100644 --- a/res/vcpkg/ffmpeg/patch/0001-avcodec-amfenc-add-query_timeout-option-for-h264-hev.patch +++ b/res/vcpkg/ffmpeg/patch/0001-avcodec-amfenc-add-query_timeout-option-for-h264-hev.patch @@ -1,7 +1,7 @@ -From f6988e5424e041ff6f6e241f4d8fa69a04c05e64 Mon Sep 17 00:00:00 2001 +From da6921d5bcb50961193526f47aa2dbe71ee5fe81 Mon Sep 17 00:00:00 2001 From: 21pages -Date: Thu, 5 Sep 2024 16:26:20 +0800 -Subject: [PATCH 1/3] avcodec/amfenc: add query_timeout option for h264/hevc +Date: Tue, 10 Dec 2024 13:40:46 +0800 +Subject: [PATCH 1/5] avcodec/amfenc: add query_timeout option for h264/hevc Signed-off-by: 21pages --- @@ -11,10 +11,10 @@ Signed-off-by: 21pages 3 files changed, 9 insertions(+) diff --git a/libavcodec/amfenc.h b/libavcodec/amfenc.h -index 2dbd378ef8..d636673a9d 100644 +index d985d01bb1..320c66919e 100644 --- a/libavcodec/amfenc.h +++ b/libavcodec/amfenc.h -@@ -89,6 +89,7 @@ typedef struct AmfContext { +@@ -91,6 +91,7 @@ typedef struct AmfContext { int quality; int b_frame_delta_qp; int ref_b_frame_delta_qp; @@ -23,40 +23,40 @@ index 2dbd378ef8..d636673a9d 100644 // Dynamic options, can be set after Init() call diff --git a/libavcodec/amfenc_h264.c b/libavcodec/amfenc_h264.c -index c1d5f4054e..415828f005 100644 +index 8edd39c633..6ad4961b2f 100644 --- a/libavcodec/amfenc_h264.c +++ b/libavcodec/amfenc_h264.c -@@ -135,6 +135,7 @@ static const AVOption options[] = { - { "aud", "Inserts AU Delimiter NAL unit", OFFSET(aud) ,AV_OPT_TYPE_BOOL, { .i64 = 0 }, 0, 1, VE }, +@@ -137,6 +137,7 @@ static const AVOption options[] = { + { "log_to_dbg", "Enable AMF logging to debug output", OFFSET(log_to_dbg) , AV_OPT_TYPE_BOOL, { .i64 = 0 }, 0, 1, VE }, + { "query_timeout", "Timeout for QueryOutput call in ms", OFFSET(query_timeout), AV_OPT_TYPE_INT64, { .i64 = -1 }, -1, 1000, VE }, //Pre Analysis options { "preanalysis", "Enable preanalysis", OFFSET(preanalysis), AV_OPT_TYPE_BOOL, {.i64 = -1 }, -1, 1, VE }, -@@ -222,6 +223,9 @@ FF_ENABLE_DEPRECATION_WARNINGS +@@ -228,6 +229,9 @@ FF_ENABLE_DEPRECATION_WARNINGS AMF_ASSIGN_PROPERTY_RATE(res, ctx->encoder, AMF_VIDEO_ENCODER_FRAMERATE, framerate); + if (ctx->query_timeout >= 0) -+ AMF_ASSIGN_PROPERTY_INT64(res, ctx->encoder, AMF_VIDEO_ENCODER_QUERY_TIMEOUT, ctx->query_timeout); ++ AMF_ASSIGN_PROPERTY_INT64(res, ctx->encoder, AMF_VIDEO_ENCODER_QUERY_TIMEOUT, ctx->query_timeout); + switch (avctx->profile) { case AV_PROFILE_H264_BASELINE: profile = AMF_VIDEO_ENCODER_PROFILE_BASELINE; diff --git a/libavcodec/amfenc_hevc.c b/libavcodec/amfenc_hevc.c -index 33a167aa52..65259d7153 100644 +index 4898824f3a..22cb95c7ce 100644 --- a/libavcodec/amfenc_hevc.c +++ b/libavcodec/amfenc_hevc.c -@@ -98,6 +98,7 @@ static const AVOption options[] = { - { "aud", "Inserts AU Delimiter NAL unit", OFFSET(aud) ,AV_OPT_TYPE_BOOL,{ .i64 = 0 }, 0, 1, VE }, +@@ -104,6 +104,7 @@ static const AVOption options[] = { + { "log_to_dbg", "Enable AMF logging to debug output", OFFSET(log_to_dbg), AV_OPT_TYPE_BOOL,{ .i64 = 0 }, 0, 1, VE }, + { "query_timeout", "Timeout for QueryOutput call in ms", OFFSET(query_timeout), AV_OPT_TYPE_INT64, { .i64 = -1 }, -1, 1000, VE }, //Pre Analysis options { "preanalysis", "Enable preanalysis", OFFSET(preanalysis), AV_OPT_TYPE_BOOL, {.i64 = -1 }, -1, 1, VE }, -@@ -183,6 +184,9 @@ FF_ENABLE_DEPRECATION_WARNINGS +@@ -194,6 +195,9 @@ FF_ENABLE_DEPRECATION_WARNINGS AMF_ASSIGN_PROPERTY_RATE(res, ctx->encoder, AMF_VIDEO_ENCODER_HEVC_FRAMERATE, framerate); diff --git a/res/vcpkg/ffmpeg/patch/0002-libavcodec-amfenc-reconfig-when-bitrate-change.patch b/res/vcpkg/ffmpeg/patch/0002-libavcodec-amfenc-reconfig-when-bitrate-change.patch index 62b86d08bd64..f2ec5df321e5 100644 --- a/res/vcpkg/ffmpeg/patch/0002-libavcodec-amfenc-reconfig-when-bitrate-change.patch +++ b/res/vcpkg/ffmpeg/patch/0002-libavcodec-amfenc-reconfig-when-bitrate-change.patch @@ -1,7 +1,7 @@ -From 6e76c57cf2c0e790228f19c88089eef110fd74aa Mon Sep 17 00:00:00 2001 +From 8d061adb7b00fc765b8001307c025437ef1cad88 Mon Sep 17 00:00:00 2001 From: 21pages Date: Thu, 5 Sep 2024 16:32:16 +0800 -Subject: [PATCH 2/3] libavcodec/amfenc: reconfig when bitrate change +Subject: [PATCH 2/5] libavcodec/amfenc: reconfig when bitrate change Signed-off-by: 21pages --- @@ -10,10 +10,10 @@ Signed-off-by: 21pages 2 files changed, 21 insertions(+) diff --git a/libavcodec/amfenc.c b/libavcodec/amfenc.c -index 061859f85c..97587fe66b 100644 +index a47aea6108..f70f0109f6 100644 --- a/libavcodec/amfenc.c +++ b/libavcodec/amfenc.c -@@ -222,6 +222,7 @@ static int amf_init_context(AVCodecContext *avctx) +@@ -275,6 +275,7 @@ static int amf_init_context(AVCodecContext *avctx) ctx->hwsurfaces_in_queue = 0; ctx->hwsurfaces_in_queue_max = 16; @@ -21,7 +21,7 @@ index 061859f85c..97587fe66b 100644 // configure AMF logger // the return of these functions indicates old state and do not affect behaviour -@@ -583,6 +584,23 @@ static void amf_release_buffer_with_frame_ref(AMFBuffer *frame_ref_storage_buffe +@@ -640,6 +641,23 @@ static void amf_release_buffer_with_frame_ref(AMFBuffer *frame_ref_storage_buffe frame_ref_storage_buffer->pVtbl->Release(frame_ref_storage_buffer); } @@ -45,7 +45,7 @@ index 061859f85c..97587fe66b 100644 int ff_amf_receive_packet(AVCodecContext *avctx, AVPacket *avpkt) { AmfContext *ctx = avctx->priv_data; -@@ -596,6 +614,8 @@ int ff_amf_receive_packet(AVCodecContext *avctx, AVPacket *avpkt) +@@ -653,6 +671,8 @@ int ff_amf_receive_packet(AVCodecContext *avctx, AVPacket *avpkt) int query_output_data_flag = 0; AMF_RESULT res_resubmit; @@ -55,10 +55,10 @@ index 061859f85c..97587fe66b 100644 return AVERROR(EINVAL); diff --git a/libavcodec/amfenc.h b/libavcodec/amfenc.h -index d636673a9d..09506ee2e0 100644 +index 320c66919e..481e0fb75d 100644 --- a/libavcodec/amfenc.h +++ b/libavcodec/amfenc.h -@@ -113,6 +113,7 @@ typedef struct AmfContext { +@@ -115,6 +115,7 @@ typedef struct AmfContext { int max_b_frames; int qvbr_quality_level; int hw_high_motion_quality_boost; diff --git a/res/vcpkg/ffmpeg/patch/0003-amf-colorspace.patch b/res/vcpkg/ffmpeg/patch/0003-amf-colorspace.patch deleted file mode 100644 index 9bcb6e6926c1..000000000000 --- a/res/vcpkg/ffmpeg/patch/0003-amf-colorspace.patch +++ /dev/null @@ -1,161 +0,0 @@ -From 14b77216106eaaff9cf701528039ae4264eaf420 Mon Sep 17 00:00:00 2001 -From: 21pages -Date: Thu, 5 Sep 2024 16:41:59 +0800 -Subject: [PATCH 3/3] amf colorspace - -Signed-off-by: 21pages ---- - libavcodec/amfenc.h | 1 + - libavcodec/amfenc_h264.c | 40 ++++++++++++++++++++++++++++++++++ - libavcodec/amfenc_hevc.c | 47 ++++++++++++++++++++++++++++++++++++++++ - 3 files changed, 88 insertions(+) - -diff --git a/libavcodec/amfenc.h b/libavcodec/amfenc.h -index 09506ee2e0..7f458b14f7 100644 ---- a/libavcodec/amfenc.h -+++ b/libavcodec/amfenc.h -@@ -24,6 +24,7 @@ - #include - #include - #include -+#include - - #include "libavutil/fifo.h" - -diff --git a/libavcodec/amfenc_h264.c b/libavcodec/amfenc_h264.c -index 415828f005..7da5a96c71 100644 ---- a/libavcodec/amfenc_h264.c -+++ b/libavcodec/amfenc_h264.c -@@ -200,6 +200,9 @@ static av_cold int amf_encode_init_h264(AVCodecContext *avctx) - AMFRate framerate; - AMFSize framesize = AMFConstructSize(avctx->width, avctx->height); - int deblocking_filter = (avctx->flags & AV_CODEC_FLAG_LOOP_FILTER) ? 1 : 0; -+ amf_int64 color_depth; -+ amf_int64 color_profile; -+ enum AVPixelFormat pix_fmt; - - if (avctx->framerate.num > 0 && avctx->framerate.den > 0) { - framerate = AMFConstructRate(avctx->framerate.num, avctx->framerate.den); -@@ -266,10 +269,47 @@ FF_ENABLE_DEPRECATION_WARNINGS - AMF_ASSIGN_PROPERTY_RATIO(res, ctx->encoder, AMF_VIDEO_ENCODER_ASPECT_RATIO, ratio); - } - -+ color_profile = AMF_VIDEO_CONVERTER_COLOR_PROFILE_UNKNOWN; - /// Color Range (Partial/TV/MPEG or Full/PC/JPEG) - if (avctx->color_range == AVCOL_RANGE_JPEG) { - AMF_ASSIGN_PROPERTY_BOOL(res, ctx->encoder, AMF_VIDEO_ENCODER_FULL_RANGE_COLOR, 1); -+ switch (avctx->colorspace) { -+ case AVCOL_SPC_SMPTE170M: -+ color_profile = AMF_VIDEO_CONVERTER_COLOR_PROFILE_FULL_601; -+ break; -+ case AVCOL_SPC_BT709: -+ color_profile = AMF_VIDEO_CONVERTER_COLOR_PROFILE_FULL_709; -+ break; -+ case AVCOL_SPC_BT2020_NCL: -+ case AVCOL_SPC_BT2020_CL: -+ color_profile = AMF_VIDEO_CONVERTER_COLOR_PROFILE_FULL_2020; -+ break; -+ } -+ } else { -+ AMF_ASSIGN_PROPERTY_BOOL(res, ctx->encoder, AMF_VIDEO_ENCODER_FULL_RANGE_COLOR, 0); -+ switch (avctx->colorspace) { -+ case AVCOL_SPC_SMPTE170M: -+ color_profile = AMF_VIDEO_CONVERTER_COLOR_PROFILE_601; -+ break; -+ case AVCOL_SPC_BT709: -+ color_profile = AMF_VIDEO_CONVERTER_COLOR_PROFILE_709; -+ break; -+ case AVCOL_SPC_BT2020_NCL: -+ case AVCOL_SPC_BT2020_CL: -+ color_profile = AMF_VIDEO_CONVERTER_COLOR_PROFILE_2020; -+ break; -+ } - } -+ pix_fmt = avctx->hw_frames_ctx ? ((AVHWFramesContext*)avctx->hw_frames_ctx->data)->sw_format : avctx->pix_fmt; -+ color_depth = AMF_COLOR_BIT_DEPTH_8; -+ if (pix_fmt == AV_PIX_FMT_P010) { -+ color_depth = AMF_COLOR_BIT_DEPTH_10; -+ } -+ -+ AMF_ASSIGN_PROPERTY_INT64(res, ctx->encoder, AMF_VIDEO_ENCODER_COLOR_BIT_DEPTH, color_depth); -+ AMF_ASSIGN_PROPERTY_INT64(res, ctx->encoder, AMF_VIDEO_ENCODER_OUTPUT_COLOR_PROFILE, color_profile); -+ AMF_ASSIGN_PROPERTY_INT64(res, ctx->encoder, AMF_VIDEO_ENCODER_OUTPUT_TRANSFER_CHARACTERISTIC, (amf_int64)avctx->color_trc); -+ AMF_ASSIGN_PROPERTY_INT64(res, ctx->encoder, AMF_VIDEO_ENCODER_OUTPUT_COLOR_PRIMARIES, (amf_int64)avctx->color_primaries); - - // autodetect rate control method - if (ctx->rate_control_mode == AMF_VIDEO_ENCODER_RATE_CONTROL_METHOD_UNKNOWN) { -diff --git a/libavcodec/amfenc_hevc.c b/libavcodec/amfenc_hevc.c -index 65259d7153..7c930d3ccc 100644 ---- a/libavcodec/amfenc_hevc.c -+++ b/libavcodec/amfenc_hevc.c -@@ -161,6 +161,9 @@ static av_cold int amf_encode_init_hevc(AVCodecContext *avctx) - AMFRate framerate; - AMFSize framesize = AMFConstructSize(avctx->width, avctx->height); - int deblocking_filter = (avctx->flags & AV_CODEC_FLAG_LOOP_FILTER) ? 1 : 0; -+ amf_int64 color_depth; -+ amf_int64 color_profile; -+ enum AVPixelFormat pix_fmt; - - if (avctx->framerate.num > 0 && avctx->framerate.den > 0) { - framerate = AMFConstructRate(avctx->framerate.num, avctx->framerate.den); -@@ -191,6 +194,9 @@ FF_ENABLE_DEPRECATION_WARNINGS - case AV_PROFILE_HEVC_MAIN: - profile = AMF_VIDEO_ENCODER_HEVC_PROFILE_MAIN; - break; -+ case AV_PROFILE_HEVC_MAIN_10: -+ profile = AMF_VIDEO_ENCODER_HEVC_PROFILE_MAIN_10; -+ break; - default: - break; - } -@@ -219,6 +225,47 @@ FF_ENABLE_DEPRECATION_WARNINGS - AMF_ASSIGN_PROPERTY_RATIO(res, ctx->encoder, AMF_VIDEO_ENCODER_HEVC_ASPECT_RATIO, ratio); - } - -+ color_profile = AMF_VIDEO_CONVERTER_COLOR_PROFILE_UNKNOWN; -+ if (avctx->color_range == AVCOL_RANGE_JPEG) { -+ AMF_ASSIGN_PROPERTY_BOOL(res, ctx->encoder, AMF_VIDEO_ENCODER_HEVC_NOMINAL_RANGE, 1); -+ switch (avctx->colorspace) { -+ case AVCOL_SPC_SMPTE170M: -+ color_profile = AMF_VIDEO_CONVERTER_COLOR_PROFILE_FULL_601; -+ break; -+ case AVCOL_SPC_BT709: -+ color_profile = AMF_VIDEO_CONVERTER_COLOR_PROFILE_FULL_709; -+ break; -+ case AVCOL_SPC_BT2020_NCL: -+ case AVCOL_SPC_BT2020_CL: -+ color_profile = AMF_VIDEO_CONVERTER_COLOR_PROFILE_FULL_2020; -+ break; -+ } -+ } else { -+ AMF_ASSIGN_PROPERTY_BOOL(res, ctx->encoder, AMF_VIDEO_ENCODER_HEVC_NOMINAL_RANGE, 0); -+ switch (avctx->colorspace) { -+ case AVCOL_SPC_SMPTE170M: -+ color_profile = AMF_VIDEO_CONVERTER_COLOR_PROFILE_601; -+ break; -+ case AVCOL_SPC_BT709: -+ color_profile = AMF_VIDEO_CONVERTER_COLOR_PROFILE_709; -+ break; -+ case AVCOL_SPC_BT2020_NCL: -+ case AVCOL_SPC_BT2020_CL: -+ color_profile = AMF_VIDEO_CONVERTER_COLOR_PROFILE_2020; -+ break; -+ } -+ } -+ pix_fmt = avctx->hw_frames_ctx ? ((AVHWFramesContext*)avctx->hw_frames_ctx->data)->sw_format : avctx->pix_fmt; -+ color_depth = AMF_COLOR_BIT_DEPTH_8; -+ if (pix_fmt == AV_PIX_FMT_P010) { -+ color_depth = AMF_COLOR_BIT_DEPTH_10; -+ } -+ -+ AMF_ASSIGN_PROPERTY_INT64(res, ctx->encoder, AMF_VIDEO_ENCODER_HEVC_COLOR_BIT_DEPTH, color_depth); -+ AMF_ASSIGN_PROPERTY_INT64(res, ctx->encoder, AMF_VIDEO_ENCODER_HEVC_OUTPUT_COLOR_PROFILE, color_profile); -+ AMF_ASSIGN_PROPERTY_INT64(res, ctx->encoder, AMF_VIDEO_ENCODER_HEVC_OUTPUT_TRANSFER_CHARACTERISTIC, (amf_int64)avctx->color_trc); -+ AMF_ASSIGN_PROPERTY_INT64(res, ctx->encoder, AMF_VIDEO_ENCODER_HEVC_OUTPUT_COLOR_PRIMARIES, (amf_int64)avctx->color_primaries); -+ - // Picture control properties - AMF_ASSIGN_PROPERTY_INT64(res, ctx->encoder, AMF_VIDEO_ENCODER_HEVC_NUM_GOPS_PER_IDR, ctx->gops_per_idr); - AMF_ASSIGN_PROPERTY_INT64(res, ctx->encoder, AMF_VIDEO_ENCODER_HEVC_GOP_SIZE, avctx->gop_size); --- -2.43.0.windows.1 - diff --git a/res/vcpkg/ffmpeg/patch/0004-videotoolbox-changing-bitrate.patch b/res/vcpkg/ffmpeg/patch/0004-videotoolbox-changing-bitrate.patch index a0b337c5bae5..58cf2993fdf2 100644 --- a/res/vcpkg/ffmpeg/patch/0004-videotoolbox-changing-bitrate.patch +++ b/res/vcpkg/ffmpeg/patch/0004-videotoolbox-changing-bitrate.patch @@ -1,18 +1,18 @@ -From 7f12898fe8fd12c1042c98b34825ab2eda89e54d Mon Sep 17 00:00:00 2001 +From d74de94b49efcf7a0b25673ace6016938d1b9272 Mon Sep 17 00:00:00 2001 From: 21pages -Date: Sun, 24 Nov 2024 12:58:39 +0800 -Subject: [PATCH 1/2] videotoolbox changing bitrate +Date: Tue, 10 Dec 2024 14:12:01 +0800 +Subject: [PATCH 3/5] videotoolbox changing bitrate Signed-off-by: 21pages --- - libavcodec/videotoolboxenc.c | 39 ++++++++++++++++++++++++++++++++++++ - 1 file changed, 39 insertions(+) + libavcodec/videotoolboxenc.c | 40 ++++++++++++++++++++++++++++++++++++ + 1 file changed, 40 insertions(+) diff --git a/libavcodec/videotoolboxenc.c b/libavcodec/videotoolboxenc.c -index 5ea9afee22..89c927cdcc 100644 +index da7b291b03..3c866177f5 100644 --- a/libavcodec/videotoolboxenc.c +++ b/libavcodec/videotoolboxenc.c -@@ -278,6 +278,8 @@ typedef struct VTEncContext { +@@ -279,6 +279,8 @@ typedef struct VTEncContext { int max_slice_bytes; int power_efficient; int max_ref_frames; @@ -20,8 +20,8 @@ index 5ea9afee22..89c927cdcc 100644 + int last_bit_rate; } VTEncContext; - static int vt_dump_encoder(AVCodecContext *avctx) -@@ -1174,6 +1176,7 @@ static int vtenc_create_encoder(AVCodecContext *avctx, + static void vtenc_free_buf_node(BufNode *info) +@@ -1180,6 +1182,7 @@ static int vtenc_create_encoder(AVCodecContext *avctx, int64_t one_second_value = 0; void *nums[2]; @@ -29,8 +29,8 @@ index 5ea9afee22..89c927cdcc 100644 int status = VTCompressionSessionCreate(kCFAllocatorDefault, avctx->width, avctx->height, -@@ -2618,6 +2621,41 @@ static int vtenc_send_frame(AVCodecContext *avctx, - return 0; +@@ -2638,6 +2641,42 @@ out: + return status; } +static void update_config(AVCodecContext *avctx) @@ -67,13 +67,14 @@ index 5ea9afee22..89c927cdcc 100644 + } + } +} ++ + static av_cold int vtenc_frame( AVCodecContext *avctx, AVPacket *pkt, -@@ -2630,6 +2668,7 @@ static av_cold int vtenc_frame( +@@ -2650,6 +2689,7 @@ static av_cold int vtenc_frame( CMSampleBufferRef buf = NULL; - ExtraSEI *sei = NULL; + ExtraSEI sei = {0}; + update_config(avctx); if (frame) { diff --git a/res/vcpkg/ffmpeg/patch/0005-mediacodec-changing-bitrate.patch b/res/vcpkg/ffmpeg/patch/0005-mediacodec-changing-bitrate.patch index 1fb369b5ceaa..4a552dda0fcd 100644 --- a/res/vcpkg/ffmpeg/patch/0005-mediacodec-changing-bitrate.patch +++ b/res/vcpkg/ffmpeg/patch/0005-mediacodec-changing-bitrate.patch @@ -1,17 +1,17 @@ -From ed73f8f6494d74ae47218f9503c7e3de385d9253 Mon Sep 17 00:00:00 2001 +From 7323bd68c1b34e9298ea557ff7a3e1883b653957 Mon Sep 17 00:00:00 2001 From: 21pages -Date: Sun, 24 Nov 2024 14:17:39 +0800 -Subject: [PATCH 1/2] mediacodec changing bitrate +Date: Tue, 10 Dec 2024 14:28:16 +0800 +Subject: [PATCH 4/5] mediacodec changing bitrate Signed-off-by: 21pages --- - libavcodec/mediacodec_wrapper.c | 97 +++++++++++++++++++++++++++++++++ + libavcodec/mediacodec_wrapper.c | 98 +++++++++++++++++++++++++++++++++ libavcodec/mediacodec_wrapper.h | 7 +++ libavcodec/mediacodecenc.c | 18 ++++++ - 3 files changed, 122 insertions(+) + 3 files changed, 123 insertions(+) diff --git a/libavcodec/mediacodec_wrapper.c b/libavcodec/mediacodec_wrapper.c -index 306359071e..7edb38a7d7 100644 +index 96c886666a..06b8504304 100644 --- a/libavcodec/mediacodec_wrapper.c +++ b/libavcodec/mediacodec_wrapper.c @@ -35,6 +35,8 @@ @@ -66,10 +66,11 @@ index 306359071e..7edb38a7d7 100644 #define JNI_GET_ENV_OR_RETURN(env, log_ctx, ret) do { \ (env) = ff_jni_get_env(log_ctx); \ if (!(env)) { \ -@@ -1761,6 +1785,69 @@ static int mediacodec_jni_signalEndOfInputStream(FFAMediaCodec *ctx) +@@ -1762,6 +1786,70 @@ static int mediacodec_jni_signalEndOfInputStream(FFAMediaCodec *ctx) return 0; } ++ +static int mediacodec_jni_setParameter(FFAMediaCodec *ctx, const char* name, int value) +{ + JNIEnv *env = NULL; @@ -136,7 +137,7 @@ index 306359071e..7edb38a7d7 100644 static const FFAMediaFormat media_format_jni = { .class = &amediaformat_class, -@@ -1820,6 +1907,8 @@ static const FFAMediaCodec media_codec_jni = { +@@ -1821,6 +1909,8 @@ static const FFAMediaCodec media_codec_jni = { .getConfigureFlagEncode = mediacodec_jni_getConfigureFlagEncode, .cleanOutputBuffers = mediacodec_jni_cleanOutputBuffers, .signalEndOfInputStream = mediacodec_jni_signalEndOfInputStream, @@ -145,7 +146,7 @@ index 306359071e..7edb38a7d7 100644 }; typedef struct FFAMediaFormatNdk { -@@ -2428,6 +2517,12 @@ static int mediacodec_ndk_signalEndOfInputStream(FFAMediaCodec *ctx) +@@ -2335,6 +2425,12 @@ static int mediacodec_ndk_signalEndOfInputStream(FFAMediaCodec *ctx) return 0; } @@ -158,7 +159,7 @@ index 306359071e..7edb38a7d7 100644 static const FFAMediaFormat media_format_ndk = { .class = &amediaformat_ndk_class, -@@ -2489,6 +2584,8 @@ static const FFAMediaCodec media_codec_ndk = { +@@ -2396,6 +2492,8 @@ static const FFAMediaCodec media_codec_ndk = { .getConfigureFlagEncode = mediacodec_ndk_getConfigureFlagEncode, .cleanOutputBuffers = mediacodec_ndk_cleanOutputBuffers, .signalEndOfInputStream = mediacodec_ndk_signalEndOfInputStream, @@ -193,19 +194,19 @@ index 11a4260497..86c64556ad 100644 enum FFAMediaFormatColorRange { diff --git a/libavcodec/mediacodecenc.c b/libavcodec/mediacodecenc.c -index d3bf27cb7f..621529d686 100644 +index 6ca3968a24..221f7360f4 100644 --- a/libavcodec/mediacodecenc.c +++ b/libavcodec/mediacodecenc.c -@@ -73,6 +73,8 @@ typedef struct MediaCodecEncContext { - int bitrate_mode; +@@ -76,6 +76,8 @@ typedef struct MediaCodecEncContext { int level; int pts_as_dts; + int extract_extradata; + + int last_bit_rate; } MediaCodecEncContext; enum { -@@ -155,6 +157,8 @@ static av_cold int mediacodec_init(AVCodecContext *avctx) +@@ -193,6 +195,8 @@ static av_cold int mediacodec_init(AVCodecContext *avctx) int ret; int gop; @@ -214,7 +215,7 @@ index d3bf27cb7f..621529d686 100644 if (s->use_ndk_codec < 0) s->use_ndk_codec = !av_jni_get_java_vm(avctx); -@@ -515,12 +519,26 @@ static int mediacodec_send(AVCodecContext *avctx, +@@ -542,11 +546,25 @@ static int mediacodec_send(AVCodecContext *avctx, return 0; } @@ -235,12 +236,11 @@ index d3bf27cb7f..621529d686 100644 { MediaCodecEncContext *s = avctx->priv_data; int ret; - int got_packet = 0; + update_config(avctx); // Return on three case: // 1. Serious error // 2. Got a packet success -- -2.34.1 +2.43.0.windows.1 diff --git a/res/vcpkg/ffmpeg/patch/0006-dlopen-libva.patch b/res/vcpkg/ffmpeg/patch/0006-dlopen-libva.patch index e13a5de11e87..a62be5a81956 100644 --- a/res/vcpkg/ffmpeg/patch/0006-dlopen-libva.patch +++ b/res/vcpkg/ffmpeg/patch/0006-dlopen-libva.patch @@ -1,25 +1,23 @@ -From 6553fc4eae5d03bc712c30ae1e7519753c37275c Mon Sep 17 00:00:00 2001 +From 95ebc0ad912447ba83cacb197f506b881f82179e Mon Sep 17 00:00:00 2001 From: 21pages -Date: Wed, 4 Dec 2024 12:53:23 +0800 -Subject: [PATCH] dlopen libva +Date: Tue, 10 Dec 2024 15:29:21 +0800 +Subject: [PATCH 1/2] dlopen libva Signed-off-by: 21pages --- - libavcodec/vaapi_decode.c | 99 +++++++----- - libavcodec/vaapi_encode.c | 176 +++++++++++--------- - libavcodec/vaapi_encode_av1.c | 13 +- + libavcodec/vaapi_decode.c | 96 ++++++----- + libavcodec/vaapi_encode.c | 173 ++++++++++--------- libavcodec/vaapi_encode_h264.c | 3 +- - libavcodec/vaapi_encode_h265.c | 5 +- - libavutil/hwcontext_vaapi.c | 288 +++++++++++++++++++++++++-------- - libavutil/hwcontext_vaapi.h | 97 +++++++++++ - libavutil/hwcontext_vulkan.c | 5 +- - 8 files changed, 494 insertions(+), 192 deletions(-) + libavcodec/vaapi_encode_h265.c | 6 +- + libavutil/hwcontext_vaapi.c | 292 ++++++++++++++++++++++++--------- + libavutil/hwcontext_vaapi.h | 96 +++++++++++ + 6 files changed, 477 insertions(+), 189 deletions(-) diff --git a/libavcodec/vaapi_decode.c b/libavcodec/vaapi_decode.c -index cca94b5336..776270588f 100644 +index a59194340f..e202b673f4 100644 --- a/libavcodec/vaapi_decode.c +++ b/libavcodec/vaapi_decode.c -@@ -37,17 +37,18 @@ int ff_vaapi_decode_make_param_buffer(AVCodecContext *avctx, +@@ -38,17 +38,18 @@ int ff_vaapi_decode_make_param_buffer(AVCodecContext *avctx, size_t size) { VAAPIDecodeContext *ctx = avctx->internal->hwaccel_priv_data; @@ -40,7 +38,7 @@ index cca94b5336..776270588f 100644 return AVERROR(EIO); } -@@ -67,6 +68,7 @@ int ff_vaapi_decode_make_slice_buffer(AVCodecContext *avctx, +@@ -69,6 +70,7 @@ int ff_vaapi_decode_make_slice_buffer(AVCodecContext *avctx, size_t slice_size) { VAAPIDecodeContext *ctx = avctx->internal->hwaccel_priv_data; @@ -48,14 +46,14 @@ index cca94b5336..776270588f 100644 VAStatus vas; int index; -@@ -85,13 +87,13 @@ int ff_vaapi_decode_make_slice_buffer(AVCodecContext *avctx, +@@ -88,13 +90,13 @@ int ff_vaapi_decode_make_slice_buffer(AVCodecContext *avctx, index = 2 * pic->nb_slices; - vas = vaCreateBuffer(ctx->hwctx->display, ctx->va_context, + vas = vaf->vaCreateBuffer(ctx->hwctx->display, ctx->va_context, VASliceParameterBufferType, - params_size, 1, (void*)params_data, + params_size, nb_params, (void*)params_data, &pic->slice_buffers[index]); if (vas != VA_STATUS_SUCCESS) { av_log(avctx, AV_LOG_ERROR, "Failed to create slice " @@ -64,7 +62,7 @@ index cca94b5336..776270588f 100644 return AVERROR(EIO); } -@@ -99,15 +101,15 @@ int ff_vaapi_decode_make_slice_buffer(AVCodecContext *avctx, +@@ -102,15 +104,15 @@ int ff_vaapi_decode_make_slice_buffer(AVCodecContext *avctx, "is %#x.\n", pic->nb_slices, params_size, pic->slice_buffers[index]); @@ -83,7 +81,7 @@ index cca94b5336..776270588f 100644 pic->slice_buffers[index]); return AVERROR(EIO); } -@@ -124,26 +126,27 @@ static void ff_vaapi_decode_destroy_buffers(AVCodecContext *avctx, +@@ -127,26 +129,27 @@ static void ff_vaapi_decode_destroy_buffers(AVCodecContext *avctx, VAAPIDecodePicture *pic) { VAAPIDecodeContext *ctx = avctx->internal->hwaccel_priv_data; @@ -115,7 +113,7 @@ index cca94b5336..776270588f 100644 } } } -@@ -152,43 +155,44 @@ int ff_vaapi_decode_issue(AVCodecContext *avctx, +@@ -155,6 +158,7 @@ int ff_vaapi_decode_issue(AVCodecContext *avctx, VAAPIDecodePicture *pic) { VAAPIDecodeContext *ctx = avctx->internal->hwaccel_priv_data; @@ -123,6 +121,7 @@ index cca94b5336..776270588f 100644 VAStatus vas; int err; +@@ -166,37 +170,37 @@ int ff_vaapi_decode_issue(AVCodecContext *avctx, av_log(avctx, AV_LOG_DEBUG, "Decode to surface %#x.\n", pic->output_surface); @@ -168,7 +167,7 @@ index cca94b5336..776270588f 100644 err = AVERROR(EIO); if (CONFIG_VAAPI_1 || ctx->hwctx->driver_quirks & AV_VAAPI_DRIVER_QUIRK_RENDER_PARAM_BUFFERS) -@@ -205,10 +209,10 @@ int ff_vaapi_decode_issue(AVCodecContext *avctx, +@@ -213,10 +217,10 @@ int ff_vaapi_decode_issue(AVCodecContext *avctx, goto exit; fail_with_picture: @@ -181,7 +180,7 @@ index cca94b5336..776270588f 100644 } fail: ff_vaapi_decode_destroy_buffers(avctx, pic); -@@ -296,6 +300,7 @@ static int vaapi_decode_find_best_format(AVCodecContext *avctx, +@@ -304,6 +308,7 @@ static int vaapi_decode_find_best_format(AVCodecContext *avctx, AVHWFramesContext *frames) { AVVAAPIDeviceContext *hwctx = device->hwctx; @@ -189,7 +188,7 @@ index cca94b5336..776270588f 100644 VAStatus vas; VASurfaceAttrib *attr; enum AVPixelFormat source_format, best_format, format; -@@ -305,11 +310,11 @@ static int vaapi_decode_find_best_format(AVCodecContext *avctx, +@@ -313,11 +318,11 @@ static int vaapi_decode_find_best_format(AVCodecContext *avctx, source_format = avctx->sw_pix_fmt; av_assert0(source_format != AV_PIX_FMT_NONE); @@ -203,7 +202,7 @@ index cca94b5336..776270588f 100644 return AVERROR(ENOSYS); } -@@ -317,11 +322,11 @@ static int vaapi_decode_find_best_format(AVCodecContext *avctx, +@@ -325,11 +330,11 @@ static int vaapi_decode_find_best_format(AVCodecContext *avctx, if (!attr) return AVERROR(ENOMEM); @@ -217,7 +216,7 @@ index cca94b5336..776270588f 100644 av_freep(&attr); return AVERROR(ENOSYS); } -@@ -463,6 +468,7 @@ static int vaapi_decode_make_config(AVCodecContext *avctx, +@@ -471,6 +476,7 @@ static int vaapi_decode_make_config(AVCodecContext *avctx, AVHWDeviceContext *device = (AVHWDeviceContext*)device_ref->data; AVVAAPIDeviceContext *hwctx = device->hwctx; @@ -225,7 +224,7 @@ index cca94b5336..776270588f 100644 codec_desc = avcodec_descriptor_get(avctx->codec_id); if (!codec_desc) { -@@ -470,7 +476,7 @@ static int vaapi_decode_make_config(AVCodecContext *avctx, +@@ -478,7 +484,7 @@ static int vaapi_decode_make_config(AVCodecContext *avctx, goto fail; } @@ -234,7 +233,7 @@ index cca94b5336..776270588f 100644 profile_list = av_malloc_array(profile_count, sizeof(VAProfile)); if (!profile_list) { -@@ -478,11 +484,11 @@ static int vaapi_decode_make_config(AVCodecContext *avctx, +@@ -486,11 +492,11 @@ static int vaapi_decode_make_config(AVCodecContext *avctx, goto fail; } @@ -248,7 +247,7 @@ index cca94b5336..776270588f 100644 err = AVERROR(ENOSYS); goto fail; } -@@ -542,12 +548,12 @@ static int vaapi_decode_make_config(AVCodecContext *avctx, +@@ -550,12 +556,12 @@ static int vaapi_decode_make_config(AVCodecContext *avctx, } } @@ -263,7 +262,7 @@ index cca94b5336..776270588f 100644 err = AVERROR(EIO); goto fail; } -@@ -626,7 +632,7 @@ fail: +@@ -638,7 +644,7 @@ fail: av_hwframe_constraints_free(&constraints); av_freep(&hwconfig); if (*va_config != VA_INVALID_ID) { @@ -272,7 +271,7 @@ index cca94b5336..776270588f 100644 *va_config = VA_INVALID_ID; } av_freep(&profile_list); -@@ -639,20 +645,21 @@ int ff_vaapi_common_frame_params(AVCodecContext *avctx, +@@ -651,12 +657,14 @@ int ff_vaapi_common_frame_params(AVCodecContext *avctx, AVHWFramesContext *hw_frames = (AVHWFramesContext *)hw_frames_ctx->data; AVHWDeviceContext *device_ctx = hw_frames->device_ctx; AVVAAPIDeviceContext *hwctx; @@ -283,11 +282,11 @@ index cca94b5336..776270588f 100644 if (device_ctx->type != AV_HWDEVICE_TYPE_VAAPI) return AVERROR(EINVAL); hwctx = device_ctx->hwctx; -- + vaf = hwctx->funcs; + err = vaapi_decode_make_config(avctx, hw_frames->device_ref, &va_config, hw_frames_ctx); - if (err) +@@ -664,7 +672,7 @@ int ff_vaapi_common_frame_params(AVCodecContext *avctx, return err; if (va_config != VA_INVALID_ID) @@ -296,7 +295,7 @@ index cca94b5336..776270588f 100644 return 0; } -@@ -660,6 +667,7 @@ int ff_vaapi_common_frame_params(AVCodecContext *avctx, +@@ -672,6 +680,7 @@ int ff_vaapi_common_frame_params(AVCodecContext *avctx, int ff_vaapi_decode_init(AVCodecContext *avctx) { VAAPIDecodeContext *ctx = avctx->internal->hwaccel_priv_data; @@ -304,16 +303,16 @@ index cca94b5336..776270588f 100644 VAStatus vas; int err; -@@ -674,13 +682,17 @@ int ff_vaapi_decode_init(AVCodecContext *avctx) +@@ -686,13 +695,18 @@ int ff_vaapi_decode_init(AVCodecContext *avctx) ctx->hwfc = ctx->frames->hwctx; ctx->device = ctx->frames->device_ctx; ctx->hwctx = ctx->device->hwctx; -- + if (!ctx->hwctx || !ctx->hwctx->funcs) { + err = AVERROR(EINVAL); + goto fail; + } + vaf = ctx->hwctx->funcs; + err = vaapi_decode_make_config(avctx, ctx->frames->device_ref, &ctx->va_config, NULL); if (err) @@ -324,7 +323,7 @@ index cca94b5336..776270588f 100644 avctx->coded_width, avctx->coded_height, VA_PROGRESSIVE, ctx->hwfc->surface_ids, -@@ -688,7 +700,7 @@ int ff_vaapi_decode_init(AVCodecContext *avctx) +@@ -700,7 +714,7 @@ int ff_vaapi_decode_init(AVCodecContext *avctx) &ctx->va_context); if (vas != VA_STATUS_SUCCESS) { av_log(avctx, AV_LOG_ERROR, "Failed to create decode " @@ -333,7 +332,7 @@ index cca94b5336..776270588f 100644 err = AVERROR(EIO); goto fail; } -@@ -706,22 +718,29 @@ fail: +@@ -718,22 +732,28 @@ fail: int ff_vaapi_decode_uninit(AVCodecContext *avctx) { VAAPIDecodeContext *ctx = avctx->internal->hwaccel_priv_data; @@ -342,7 +341,6 @@ index cca94b5336..776270588f 100644 + if (ctx->hwctx && ctx->hwctx->funcs) + vaf = ctx->hwctx->funcs; -+ + if (!vaf) + return 0; + @@ -368,10 +366,10 @@ index cca94b5336..776270588f 100644 } diff --git a/libavcodec/vaapi_encode.c b/libavcodec/vaapi_encode.c -index b8765a19c7..65eb8740a8 100644 +index 16a9a364f0..ccf6fa59d6 100644 --- a/libavcodec/vaapi_encode.c +++ b/libavcodec/vaapi_encode.c -@@ -44,6 +44,7 @@ static int vaapi_encode_make_packed_header(AVCodecContext *avctx, +@@ -43,6 +43,7 @@ static int vaapi_encode_make_packed_header(AVCodecContext *avctx, int type, char *data, size_t bit_len) { VAAPIEncodeContext *ctx = avctx->priv_data; @@ -379,7 +377,7 @@ index b8765a19c7..65eb8740a8 100644 VAStatus vas; VABufferID param_buffer, data_buffer; VABufferID *tmp; -@@ -58,24 +59,24 @@ static int vaapi_encode_make_packed_header(AVCodecContext *avctx, +@@ -57,24 +58,24 @@ static int vaapi_encode_make_packed_header(AVCodecContext *avctx, return AVERROR(ENOMEM); pic->param_buffers = tmp; @@ -408,7 +406,7 @@ index b8765a19c7..65eb8740a8 100644 return AVERROR(EIO); } pic->param_buffers[pic->nb_param_buffers++] = data_buffer; -@@ -90,6 +91,7 @@ static int vaapi_encode_make_param_buffer(AVCodecContext *avctx, +@@ -89,6 +90,7 @@ static int vaapi_encode_make_param_buffer(AVCodecContext *avctx, int type, char *data, size_t len) { VAAPIEncodeContext *ctx = avctx->priv_data; @@ -416,14 +414,13 @@ index b8765a19c7..65eb8740a8 100644 VAStatus vas; VABufferID *tmp; VABufferID buffer; -@@ -99,11 +101,11 @@ static int vaapi_encode_make_param_buffer(AVCodecContext *avctx, +@@ -98,11 +100,11 @@ static int vaapi_encode_make_param_buffer(AVCodecContext *avctx, return AVERROR(ENOMEM); pic->param_buffers = tmp; - vas = vaCreateBuffer(ctx->hwctx->display, ctx->va_context, -- type, len, 1, data, &buffer); + vas = vaf->vaCreateBuffer(ctx->hwctx->display, ctx->va_context, -+ type, len, 1, data, &buffer); + type, len, 1, data, &buffer); if (vas != VA_STATUS_SUCCESS) { av_log(avctx, AV_LOG_ERROR, "Failed to create parameter buffer " - "(type %d): %d (%s).\n", type, vas, vaErrorStr(vas)); @@ -431,21 +428,21 @@ index b8765a19c7..65eb8740a8 100644 return AVERROR(EIO); } pic->param_buffers[pic->nb_param_buffers++] = buffer; -@@ -140,6 +142,7 @@ static int vaapi_encode_wait(AVCodecContext *avctx, - VAAPIEncodePicture *pic) - { +@@ -141,6 +143,7 @@ static int vaapi_encode_wait(AVCodecContext *avctx, FFHWBaseEncodePicture *base_ + FFHWBaseEncodeContext *base_ctx = avctx->priv_data; + #endif VAAPIEncodeContext *ctx = avctx->priv_data; + VAAPIDynLoadFunctions *vaf = ctx->hwctx->funcs; + VAAPIEncodePicture *pic = base_pic->priv; VAStatus vas; - av_assert0(pic->encode_issued); -@@ -154,22 +157,22 @@ static int vaapi_encode_wait(AVCodecContext *avctx, - pic->encode_order, pic->input_surface); +@@ -156,22 +159,22 @@ static int vaapi_encode_wait(AVCodecContext *avctx, FFHWBaseEncodePicture *base_ + base_pic->encode_order, pic->input_surface); #if VA_CHECK_VERSION(1, 9, 0) -- if (ctx->has_sync_buffer_func) { +- if (base_ctx->async_encode) { - vas = vaSyncBuffer(ctx->hwctx->display, -+ if (ctx->has_sync_buffer_func && vaf->vaSyncBuffer) { ++ if (base_ctx->async_encode && vaf->vaSyncBuffer) { + vas = vaf->vaSyncBuffer(ctx->hwctx->display, pic->output_buffer, VA_TIMEOUT_INFINITE); @@ -467,15 +464,15 @@ index b8765a19c7..65eb8740a8 100644 return AVERROR(EIO); } } -@@ -267,6 +270,7 @@ static int vaapi_encode_issue(AVCodecContext *avctx, - VAAPIEncodePicture *pic) +@@ -270,6 +273,7 @@ static int vaapi_encode_issue(AVCodecContext *avctx, { - VAAPIEncodeContext *ctx = avctx->priv_data; + FFHWBaseEncodeContext *base_ctx = avctx->priv_data; + VAAPIEncodeContext *ctx = avctx->priv_data; + VAAPIDynLoadFunctions *vaf = ctx->hwctx->funcs; + VAAPIEncodePicture *pic = base_pic->priv; VAAPIEncodeSlice *slice; VAStatus vas; - int err, i; -@@ -594,28 +598,28 @@ static int vaapi_encode_issue(AVCodecContext *avctx, +@@ -587,28 +591,28 @@ static int vaapi_encode_issue(AVCodecContext *avctx, } #endif @@ -506,11 +503,11 @@ index b8765a19c7..65eb8740a8 100644 if (vas != VA_STATUS_SUCCESS) { av_log(avctx, AV_LOG_ERROR, "Failed to end picture encode issue: " - "%d (%s).\n", vas, vaErrorStr(vas)); -+ "%d (%s).\n", vas, vaf->vaErrorStr(vas)); ++ "%d (%s).\n", vas, vaf->vaErrorStr(vas)); err = AVERROR(EIO); // vaRenderPicture() has been called here, so we should not destroy // the parameter buffers unless separate destruction is required. -@@ -629,12 +633,12 @@ static int vaapi_encode_issue(AVCodecContext *avctx, +@@ -622,12 +626,12 @@ static int vaapi_encode_issue(AVCodecContext *avctx, if (CONFIG_VAAPI_1 || ctx->hwctx->driver_quirks & AV_VAAPI_DRIVER_QUIRK_RENDER_PARAM_BUFFERS) { for (i = 0; i < pic->nb_param_buffers; i++) { @@ -525,7 +522,7 @@ index b8765a19c7..65eb8740a8 100644 // And ignore. } } -@@ -645,10 +649,10 @@ static int vaapi_encode_issue(AVCodecContext *avctx, +@@ -636,10 +640,10 @@ static int vaapi_encode_issue(AVCodecContext *avctx, return 0; fail_with_picture: @@ -538,7 +535,7 @@ index b8765a19c7..65eb8740a8 100644 if (pic->slices) { for (i = 0; i < pic->nb_slices; i++) av_freep(&pic->slices[i].codec_slice_params); -@@ -707,16 +711,17 @@ static int vaapi_encode_set_output_property(AVCodecContext *avctx, +@@ -657,16 +661,17 @@ fail_at_end: static int vaapi_encode_get_coded_buffer_size(AVCodecContext *avctx, VABufferID buf_id) { VAAPIEncodeContext *ctx = avctx->priv_data; @@ -558,7 +555,7 @@ index b8765a19c7..65eb8740a8 100644 err = AVERROR(EIO); return err; } -@@ -724,10 +729,10 @@ static int vaapi_encode_get_coded_buffer_size(AVCodecContext *avctx, VABufferID +@@ -674,10 +679,10 @@ static int vaapi_encode_get_coded_buffer_size(AVCodecContext *avctx, VABufferID for (buf = buf_list; buf; buf = buf->next) size += buf->size; @@ -571,7 +568,7 @@ index b8765a19c7..65eb8740a8 100644 err = AVERROR(EIO); return err; } -@@ -739,15 +744,16 @@ static int vaapi_encode_get_coded_buffer_data(AVCodecContext *avctx, +@@ -689,15 +694,16 @@ static int vaapi_encode_get_coded_buffer_data(AVCodecContext *avctx, VABufferID buf_id, uint8_t **dst) { VAAPIEncodeContext *ctx = avctx->priv_data; @@ -590,7 +587,7 @@ index b8765a19c7..65eb8740a8 100644 err = AVERROR(EIO); return err; } -@@ -760,10 +766,10 @@ static int vaapi_encode_get_coded_buffer_data(AVCodecContext *avctx, +@@ -710,10 +716,10 @@ static int vaapi_encode_get_coded_buffer_data(AVCodecContext *avctx, *dst += buf->size; } @@ -603,15 +600,15 @@ index b8765a19c7..65eb8740a8 100644 err = AVERROR(EIO); return err; } -@@ -1552,6 +1558,7 @@ static const VAEntrypoint vaapi_encode_entrypoints_low_power[] = { - static av_cold int vaapi_encode_profile_entrypoint(AVCodecContext *avctx) +@@ -936,6 +942,7 @@ static av_cold int vaapi_encode_profile_entrypoint(AVCodecContext *avctx) { - VAAPIEncodeContext *ctx = avctx->priv_data; + FFHWBaseEncodeContext *base_ctx = avctx->priv_data; + VAAPIEncodeContext *ctx = avctx->priv_data; + VAAPIDynLoadFunctions *vaf = ctx->hwctx->funcs; - VAProfile *va_profiles = NULL; - VAEntrypoint *va_entrypoints = NULL; + VAProfile *va_profiles = NULL; + VAEntrypoint *va_entrypoints = NULL; VAStatus vas; -@@ -1593,16 +1600,16 @@ static av_cold int vaapi_encode_profile_entrypoint(AVCodecContext *avctx) +@@ -977,16 +984,16 @@ static av_cold int vaapi_encode_profile_entrypoint(AVCodecContext *avctx) av_log(avctx, AV_LOG_VERBOSE, "Input surface format is %s.\n", desc->name); @@ -631,7 +628,7 @@ index b8765a19c7..65eb8740a8 100644 err = AVERROR_EXTERNAL; goto fail; } -@@ -1623,7 +1630,7 @@ static av_cold int vaapi_encode_profile_entrypoint(AVCodecContext *avctx) +@@ -1007,7 +1014,7 @@ static av_cold int vaapi_encode_profile_entrypoint(AVCodecContext *avctx) continue; #if VA_CHECK_VERSION(1, 0, 0) @@ -640,7 +637,7 @@ index b8765a19c7..65eb8740a8 100644 #else profile_string = "(no profile names)"; #endif -@@ -1653,18 +1660,18 @@ static av_cold int vaapi_encode_profile_entrypoint(AVCodecContext *avctx) +@@ -1037,18 +1044,18 @@ static av_cold int vaapi_encode_profile_entrypoint(AVCodecContext *avctx) av_log(avctx, AV_LOG_VERBOSE, "Using VAAPI profile %s (%d).\n", profile_string, ctx->va_profile); @@ -662,7 +659,7 @@ index b8765a19c7..65eb8740a8 100644 err = AVERROR_EXTERNAL; goto fail; } -@@ -1686,7 +1693,7 @@ static av_cold int vaapi_encode_profile_entrypoint(AVCodecContext *avctx) +@@ -1070,7 +1077,7 @@ static av_cold int vaapi_encode_profile_entrypoint(AVCodecContext *avctx) ctx->va_entrypoint = va_entrypoints[i]; #if VA_CHECK_VERSION(1, 0, 0) @@ -671,7 +668,7 @@ index b8765a19c7..65eb8740a8 100644 #else entrypoint_string = "(no entrypoint names)"; #endif -@@ -1711,12 +1718,12 @@ static av_cold int vaapi_encode_profile_entrypoint(AVCodecContext *avctx) +@@ -1095,12 +1102,12 @@ static av_cold int vaapi_encode_profile_entrypoint(AVCodecContext *avctx) } rt_format_attr = (VAConfigAttrib) { VAConfigAttribRTFormat }; @@ -686,7 +683,7 @@ index b8765a19c7..65eb8740a8 100644 err = AVERROR_EXTERNAL; goto fail; } -@@ -1773,6 +1780,7 @@ static const VAAPIEncodeRCMode vaapi_encode_rc_modes[] = { +@@ -1157,6 +1164,7 @@ static const VAAPIEncodeRCMode vaapi_encode_rc_modes[] = { static av_cold int vaapi_encode_init_rate_control(AVCodecContext *avctx) { VAAPIEncodeContext *ctx = avctx->priv_data; @@ -694,7 +691,7 @@ index b8765a19c7..65eb8740a8 100644 uint32_t supported_va_rc_modes; const VAAPIEncodeRCMode *rc_mode; int64_t rc_bits_per_second; -@@ -1786,12 +1794,12 @@ static av_cold int vaapi_encode_init_rate_control(AVCodecContext *avctx) +@@ -1170,12 +1178,12 @@ static av_cold int vaapi_encode_init_rate_control(AVCodecContext *avctx) VAStatus vas; char supported_rc_modes_string[64]; @@ -709,7 +706,7 @@ index b8765a19c7..65eb8740a8 100644 return AVERROR_EXTERNAL; } if (rc_attr.value == VA_ATTRIB_NOT_SUPPORTED) { -@@ -2132,6 +2140,7 @@ static av_cold int vaapi_encode_init_max_frame_size(AVCodecContext *avctx) +@@ -1516,6 +1524,7 @@ static av_cold int vaapi_encode_init_max_frame_size(AVCodecContext *avctx) { #if VA_CHECK_VERSION(1, 5, 0) VAAPIEncodeContext *ctx = avctx->priv_data; @@ -717,7 +714,7 @@ index b8765a19c7..65eb8740a8 100644 VAConfigAttrib attr = { VAConfigAttribMaxFrameSize }; VAStatus vas; -@@ -2142,14 +2151,14 @@ static av_cold int vaapi_encode_init_max_frame_size(AVCodecContext *avctx) +@@ -1526,14 +1535,14 @@ static av_cold int vaapi_encode_init_max_frame_size(AVCodecContext *avctx) return AVERROR(EINVAL); } @@ -734,15 +731,15 @@ index b8765a19c7..65eb8740a8 100644 return AVERROR_EXTERNAL; } -@@ -2188,18 +2197,19 @@ static av_cold int vaapi_encode_init_max_frame_size(AVCodecContext *avctx) - static av_cold int vaapi_encode_init_gop_structure(AVCodecContext *avctx) +@@ -1573,18 +1582,19 @@ static av_cold int vaapi_encode_init_gop_structure(AVCodecContext *avctx) { - VAAPIEncodeContext *ctx = avctx->priv_data; + FFHWBaseEncodeContext *base_ctx = avctx->priv_data; + VAAPIEncodeContext *ctx = avctx->priv_data; + VAAPIDynLoadFunctions *vaf = ctx->hwctx->funcs; VAStatus vas; VAConfigAttrib attr = { VAConfigAttribEncMaxRefFrames }; uint32_t ref_l0, ref_l1; - int prediction_pre_only; + int prediction_pre_only, err; - vas = vaGetConfigAttributes(ctx->hwctx->display, + vas = vaf->vaGetConfigAttributes(ctx->hwctx->display, @@ -756,8 +753,8 @@ index b8765a19c7..65eb8740a8 100644 return AVERROR_EXTERNAL; } -@@ -2217,13 +2227,13 @@ static av_cold int vaapi_encode_init_gop_structure(AVCodecContext *avctx) - if (!(ctx->codec->flags & FLAG_INTRA_ONLY || +@@ -1602,13 +1612,13 @@ static av_cold int vaapi_encode_init_gop_structure(AVCodecContext *avctx) + if (!(ctx->codec->flags & FF_HW_FLAG_INTRA_ONLY || avctx->gop_size <= 1)) { attr = (VAConfigAttrib) { VAConfigAttribPredictionDirection }; - vas = vaGetConfigAttributes(ctx->hwctx->display, @@ -772,22 +769,15 @@ index b8765a19c7..65eb8740a8 100644 return AVERROR_EXTERNAL; } else if (attr.value == VA_ATTRIB_NOT_SUPPORTED) { av_log(avctx, AV_LOG_VERBOSE, "Driver does not report any additional " -@@ -2409,12 +2419,14 @@ static av_cold int vaapi_encode_init_tile_slice_structure(AVCodecContext *avctx, - av_log(avctx, AV_LOG_VERBOSE, "Encoding pictures with %d x %d tile.\n", - ctx->tile_rows, ctx->tile_cols); - -+ - return 0; - } - - static av_cold int vaapi_encode_init_slice_structure(AVCodecContext *avctx) +@@ -1758,6 +1768,7 @@ static av_cold int vaapi_encode_init_slice_structure(AVCodecContext *avctx) { + FFHWBaseEncodeContext *base_ctx = avctx->priv_data; VAAPIEncodeContext *ctx = avctx->priv_data; + VAAPIDynLoadFunctions *vaf = ctx->hwctx->funcs; VAConfigAttrib attr[3] = { { VAConfigAttribEncMaxSlices }, { VAConfigAttribEncSliceStructure }, #if VA_CHECK_VERSION(1, 1, 0) -@@ -2446,13 +2458,13 @@ static av_cold int vaapi_encode_init_slice_structure(AVCodecContext *avctx) +@@ -1789,13 +1800,13 @@ static av_cold int vaapi_encode_init_slice_structure(AVCodecContext *avctx) return 0; } @@ -803,7 +793,7 @@ index b8765a19c7..65eb8740a8 100644 return AVERROR_EXTERNAL; } max_slices = attr[0].value; -@@ -2506,16 +2518,17 @@ static av_cold int vaapi_encode_init_slice_structure(AVCodecContext *avctx) +@@ -1849,16 +1860,17 @@ static av_cold int vaapi_encode_init_slice_structure(AVCodecContext *avctx) static av_cold int vaapi_encode_init_packed_headers(AVCodecContext *avctx) { VAAPIEncodeContext *ctx = avctx->priv_data; @@ -823,7 +813,7 @@ index b8765a19c7..65eb8740a8 100644 return AVERROR_EXTERNAL; } -@@ -2567,17 +2580,18 @@ static av_cold int vaapi_encode_init_quality(AVCodecContext *avctx) +@@ -1910,17 +1922,18 @@ static av_cold int vaapi_encode_init_quality(AVCodecContext *avctx) { #if VA_CHECK_VERSION(0, 36, 0) VAAPIEncodeContext *ctx = avctx->priv_data; @@ -844,10 +834,10 @@ index b8765a19c7..65eb8740a8 100644 return AVERROR_EXTERNAL; } -@@ -2614,16 +2628,17 @@ static av_cold int vaapi_encode_init_roi(AVCodecContext *avctx) - { +@@ -1958,16 +1971,17 @@ static av_cold int vaapi_encode_init_roi(AVCodecContext *avctx) #if VA_CHECK_VERSION(1, 0, 0) - VAAPIEncodeContext *ctx = avctx->priv_data; + FFHWBaseEncodeContext *base_ctx = avctx->priv_data; + VAAPIEncodeContext *ctx = avctx->priv_data; + VAAPIDynLoadFunctions *vaf = ctx->hwctx->funcs; VAStatus vas; VAConfigAttrib attr = { VAConfigAttribEncROI }; @@ -864,7 +854,7 @@ index b8765a19c7..65eb8740a8 100644 return AVERROR_EXTERNAL; } -@@ -2648,10 +2663,11 @@ static void vaapi_encode_free_output_buffer(FFRefStructOpaque opaque, +@@ -1992,10 +2006,11 @@ static void vaapi_encode_free_output_buffer(FFRefStructOpaque opaque, { AVCodecContext *avctx = opaque.nc; VAAPIEncodeContext *ctx = avctx->priv_data; @@ -877,22 +867,22 @@ index b8765a19c7..65eb8740a8 100644 av_log(avctx, AV_LOG_DEBUG, "Freed output buffer %#x\n", buffer_id); } -@@ -2660,6 +2676,7 @@ static int vaapi_encode_alloc_output_buffer(FFRefStructOpaque opaque, void *obj) - { +@@ -2005,6 +2020,7 @@ static int vaapi_encode_alloc_output_buffer(FFRefStructOpaque opaque, void *obj) AVCodecContext *avctx = opaque.nc; - VAAPIEncodeContext *ctx = avctx->priv_data; + FFHWBaseEncodeContext *base_ctx = avctx->priv_data; + VAAPIEncodeContext *ctx = avctx->priv_data; + VAAPIDynLoadFunctions *vaf = ctx->hwctx->funcs; VABufferID *buffer_id = obj; VAStatus vas; -@@ -2667,13 +2684,13 @@ static int vaapi_encode_alloc_output_buffer(FFRefStructOpaque opaque, void *obj) +@@ -2012,13 +2028,13 @@ static int vaapi_encode_alloc_output_buffer(FFRefStructOpaque opaque, void *obj) // to hold the largest possible compressed frame. We assume here // that the uncompressed frame plus some header data is an upper // bound on that. - vas = vaCreateBuffer(ctx->hwctx->display, ctx->va_context, + vas = vaf->vaCreateBuffer(ctx->hwctx->display, ctx->va_context, VAEncCodedBufferType, - 3 * ctx->surface_width * ctx->surface_height + + 3 * base_ctx->surface_width * base_ctx->surface_height + (1 << 16), 1, 0, buffer_id); if (vas != VA_STATUS_SUCCESS) { av_log(avctx, AV_LOG_ERROR, "Failed to create bitstream " @@ -901,17 +891,17 @@ index b8765a19c7..65eb8740a8 100644 return AVERROR(ENOMEM); } -@@ -2773,6 +2790,7 @@ static av_cold int vaapi_encode_create_recon_frames(AVCodecContext *avctx) - av_cold int ff_vaapi_encode_init(AVCodecContext *avctx) +@@ -2092,6 +2108,7 @@ av_cold int ff_vaapi_encode_init(AVCodecContext *avctx) { - VAAPIEncodeContext *ctx = avctx->priv_data; + FFHWBaseEncodeContext *base_ctx = avctx->priv_data; + VAAPIEncodeContext *ctx = avctx->priv_data; + VAAPIDynLoadFunctions *vaf = NULL; AVVAAPIFramesContext *recon_hwctx = NULL; VAStatus vas; int err; -@@ -2808,6 +2826,12 @@ av_cold int ff_vaapi_encode_init(AVCodecContext *avctx) - ctx->device = (AVHWDeviceContext*)ctx->device_ref->data; - ctx->hwctx = ctx->device->hwctx; +@@ -2107,6 +2124,12 @@ av_cold int ff_vaapi_encode_init(AVCodecContext *avctx) + + ctx->hwctx = base_ctx->device->hwctx; + if (!ctx->hwctx || !ctx->hwctx->funcs) { + err = AVERROR(EINVAL); @@ -919,10 +909,10 @@ index b8765a19c7..65eb8740a8 100644 + } + vaf = ctx->hwctx->funcs; + - ctx->tail_pkt = av_packet_alloc(); - if (!ctx->tail_pkt) { - err = AVERROR(ENOMEM); -@@ -2864,13 +2888,13 @@ av_cold int ff_vaapi_encode_init(AVCodecContext *avctx) + err = vaapi_encode_profile_entrypoint(avctx); + if (err < 0) + goto fail; +@@ -2157,13 +2180,13 @@ av_cold int ff_vaapi_encode_init(AVCodecContext *avctx) goto fail; } @@ -938,16 +928,16 @@ index b8765a19c7..65eb8740a8 100644 err = AVERROR(EIO); goto fail; } -@@ -2880,7 +2904,7 @@ av_cold int ff_vaapi_encode_init(AVCodecContext *avctx) +@@ -2173,7 +2196,7 @@ av_cold int ff_vaapi_encode_init(AVCodecContext *avctx) goto fail; - recon_hwctx = ctx->recon_frames->hwctx; + recon_hwctx = base_ctx->recon_frames->hwctx; - vas = vaCreateContext(ctx->hwctx->display, ctx->va_config, + vas = vaf->vaCreateContext(ctx->hwctx->display, ctx->va_config, - ctx->surface_width, ctx->surface_height, + base_ctx->surface_width, base_ctx->surface_height, VA_PROGRESSIVE, recon_hwctx->surface_ids, -@@ -2888,7 +2912,7 @@ av_cold int ff_vaapi_encode_init(AVCodecContext *avctx) +@@ -2181,7 +2204,7 @@ av_cold int ff_vaapi_encode_init(AVCodecContext *avctx) &ctx->va_context); if (vas != VA_STATUS_SUCCESS) { av_log(avctx, AV_LOG_ERROR, "Failed to create encode pipeline " @@ -956,32 +946,32 @@ index b8765a19c7..65eb8740a8 100644 err = AVERROR(EIO); goto fail; } -@@ -2962,14 +2986,16 @@ av_cold int ff_vaapi_encode_init(AVCodecContext *avctx) +@@ -2255,14 +2278,16 @@ av_cold int ff_vaapi_encode_init(AVCodecContext *avctx) #if VA_CHECK_VERSION(1, 9, 0) // check vaSyncBuffer function - vas = vaSyncBuffer(ctx->hwctx->display, VA_INVALID_ID, 0); - if (vas != VA_STATUS_ERROR_UNIMPLEMENTED) { -- ctx->has_sync_buffer_func = 1; -- ctx->encode_fifo = av_fifo_alloc2(ctx->async_depth, -- sizeof(VAAPIEncodePicture *), -- 0); -- if (!ctx->encode_fifo) +- base_ctx->async_encode = 1; +- base_ctx->encode_fifo = av_fifo_alloc2(base_ctx->async_depth, +- sizeof(VAAPIEncodePicture*), +- 0); +- if (!base_ctx->encode_fifo) - return AVERROR(ENOMEM); + if (vaf->vaSyncBuffer) { + vas = vaf->vaSyncBuffer(ctx->hwctx->display, VA_INVALID_ID, 0); + if (vas != VA_STATUS_ERROR_UNIMPLEMENTED) { -+ ctx->has_sync_buffer_func = 1; -+ ctx->encode_fifo = av_fifo_alloc2(ctx->async_depth, -+ sizeof(VAAPIEncodePicture *), -+ 0); -+ if (!ctx->encode_fifo) ++ base_ctx->async_encode = 1; ++ base_ctx->encode_fifo = av_fifo_alloc2(base_ctx->async_depth, ++ sizeof(VAAPIEncodePicture*), ++ 0); ++ if (!base_ctx->encode_fifo) + return AVERROR(ENOMEM); + } } #endif -@@ -2997,14 +3023,14 @@ av_cold int ff_vaapi_encode_close(AVCodecContext *avctx) +@@ -2291,14 +2316,14 @@ av_cold int ff_vaapi_encode_close(AVCodecContext *avctx) ff_refstruct_pool_uninit(&ctx->output_buffer_pool); if (ctx->va_context != VA_INVALID_ID) { @@ -1000,71 +990,11 @@ index b8765a19c7..65eb8740a8 100644 ctx->va_config = VA_INVALID_ID; } -diff --git a/libavcodec/vaapi_encode_av1.c b/libavcodec/vaapi_encode_av1.c -index a46b882ab9..2e64611ab3 100644 ---- a/libavcodec/vaapi_encode_av1.c -+++ b/libavcodec/vaapi_encode_av1.c -@@ -766,6 +766,7 @@ static av_cold int vaapi_encode_av1_init(AVCodecContext *avctx) - { - VAAPIEncodeContext *ctx = avctx->priv_data; - VAAPIEncodeAV1Context *priv = avctx->priv_data; -+ VAAPIDynLoadFunctions *vaf = ctx->hwctx->funcs; - VAConfigAttrib attr; - VAStatus vas; - int ret; -@@ -791,13 +792,13 @@ static av_cold int vaapi_encode_av1_init(AVCodecContext *avctx) - return ret; - - attr.type = VAConfigAttribEncAV1; -- vas = vaGetConfigAttributes(ctx->hwctx->display, -+ vas = vaf->vaGetConfigAttributes(ctx->hwctx->display, - ctx->va_profile, - ctx->va_entrypoint, - &attr, 1); - if (vas != VA_STATUS_SUCCESS) { - av_log(avctx, AV_LOG_ERROR, "Failed to query " -- "config attribute: %d (%s).\n", vas, vaErrorStr(vas)); -+ "config attribute: %d (%s).\n", vas, vaf->vaErrorStr(vas)); - return AVERROR_EXTERNAL; - } else if (attr.value == VA_ATTRIB_NOT_SUPPORTED) { - priv->attr.value = 0; -@@ -808,13 +809,13 @@ static av_cold int vaapi_encode_av1_init(AVCodecContext *avctx) - } - - attr.type = VAConfigAttribEncAV1Ext1; -- vas = vaGetConfigAttributes(ctx->hwctx->display, -+ vas = vaf->vaGetConfigAttributes(ctx->hwctx->display, - ctx->va_profile, - ctx->va_entrypoint, - &attr, 1); - if (vas != VA_STATUS_SUCCESS) { - av_log(avctx, AV_LOG_ERROR, "Failed to query " -- "config attribute: %d (%s).\n", vas, vaErrorStr(vas)); -+ "config attribute: %d (%s).\n", vas, vaf->vaErrorStr(vas)); - return AVERROR_EXTERNAL; - } else if (attr.value == VA_ATTRIB_NOT_SUPPORTED) { - priv->attr_ext1.value = 0; -@@ -826,13 +827,13 @@ static av_cold int vaapi_encode_av1_init(AVCodecContext *avctx) - - /** This attr provides essential indicators, return error if not support. */ - attr.type = VAConfigAttribEncAV1Ext2; -- vas = vaGetConfigAttributes(ctx->hwctx->display, -+ vas = vaf->vaGetConfigAttributes(ctx->hwctx->display, - ctx->va_profile, - ctx->va_entrypoint, - &attr, 1); - if (vas != VA_STATUS_SUCCESS || attr.value == VA_ATTRIB_NOT_SUPPORTED) { - av_log(avctx, AV_LOG_ERROR, "Failed to query " -- "config attribute: %d (%s).\n", vas, vaErrorStr(vas)); -+ "config attribute: %d (%s).\n", vas, vaf->vaErrorStr(vas)); - return AVERROR_EXTERNAL; - } else { - priv->attr_ext2.value = attr.value; diff --git a/libavcodec/vaapi_encode_h264.c b/libavcodec/vaapi_encode_h264.c -index 37df9103ae..b83e45d333 100644 +index fb87b68bec..6d4ce630ce 100644 --- a/libavcodec/vaapi_encode_h264.c +++ b/libavcodec/vaapi_encode_h264.c -@@ -1083,6 +1083,7 @@ static int vaapi_encode_h264_init_slice_params(AVCodecContext *avctx, +@@ -868,6 +868,7 @@ static int vaapi_encode_h264_init_slice_params(AVCodecContext *avctx, static av_cold int vaapi_encode_h264_configure(AVCodecContext *avctx) { VAAPIEncodeContext *ctx = avctx->priv_data; @@ -1072,7 +1002,7 @@ index 37df9103ae..b83e45d333 100644 VAAPIEncodeH264Context *priv = avctx->priv_data; int err; -@@ -1134,7 +1135,7 @@ static av_cold int vaapi_encode_h264_configure(AVCodecContext *avctx) +@@ -919,7 +920,7 @@ static av_cold int vaapi_encode_h264_configure(AVCodecContext *avctx) vaapi_encode_h264_sei_identifier_uuid, sizeof(priv->sei_identifier.uuid_iso_iec_11578)); @@ -1082,18 +1012,19 @@ index 37df9103ae..b83e45d333 100644 driver = "unknown driver"; diff --git a/libavcodec/vaapi_encode_h265.c b/libavcodec/vaapi_encode_h265.c -index c4aabbf5ed..9bb85af810 100644 +index 2283bcc0b4..7c624f99a9 100644 --- a/libavcodec/vaapi_encode_h265.c +++ b/libavcodec/vaapi_encode_h265.c -@@ -1199,6 +1199,7 @@ static int vaapi_encode_h265_init_slice_params(AVCodecContext *avctx, +@@ -899,6 +899,8 @@ static int vaapi_encode_h265_init_slice_params(AVCodecContext *avctx, static av_cold int vaapi_encode_h265_get_encoder_caps(AVCodecContext *avctx) { - VAAPIEncodeContext *ctx = avctx->priv_data; + FFHWBaseEncodeContext *base_ctx = avctx->priv_data; ++ VAAPIEncodeContext *ctx = avctx->priv_data; + VAAPIDynLoadFunctions *vaf = ctx->hwctx->funcs; - VAAPIEncodeH265Context *priv = avctx->priv_data; + VAAPIEncodeH265Context *priv = avctx->priv_data; #if VA_CHECK_VERSION(1, 13, 0) -@@ -1208,7 +1209,7 @@ static av_cold int vaapi_encode_h265_get_encoder_caps(AVCodecContext *avctx) +@@ -909,7 +911,7 @@ static av_cold int vaapi_encode_h265_get_encoder_caps(AVCodecContext *avctx) VAStatus vas; attr.type = VAConfigAttribEncHEVCFeatures; @@ -1102,7 +1033,7 @@ index c4aabbf5ed..9bb85af810 100644 ctx->va_entrypoint, &attr, 1); if (vas != VA_STATUS_SUCCESS) { av_log(avctx, AV_LOG_ERROR, "Failed to query encoder " -@@ -1222,7 +1223,7 @@ static av_cold int vaapi_encode_h265_get_encoder_caps(AVCodecContext *avctx) +@@ -923,7 +925,7 @@ static av_cold int vaapi_encode_h265_get_encoder_caps(AVCodecContext *avctx) } attr.type = VAConfigAttribEncHEVCBlockSizes; @@ -1112,19 +1043,18 @@ index c4aabbf5ed..9bb85af810 100644 if (vas != VA_STATUS_SUCCESS) { av_log(avctx, AV_LOG_ERROR, "Failed to query encoder " diff --git a/libavutil/hwcontext_vaapi.c b/libavutil/hwcontext_vaapi.c -index 95a68e62c5..0e42a36346 100644 +index 95aa38d9d2..13451e8ad7 100644 --- a/libavutil/hwcontext_vaapi.c +++ b/libavutil/hwcontext_vaapi.c -@@ -47,7 +47,7 @@ typedef HRESULT (WINAPI *PFN_CREATE_DXGI_FACTORY)(REFIID riid, void **ppFactory) - #if HAVE_UNISTD_H +@@ -48,6 +48,7 @@ typedef HRESULT (WINAPI *PFN_CREATE_DXGI_FACTORY)(REFIID riid, void **ppFactory) # include #endif -- + +#include #include "avassert.h" #include "buffer.h" -@@ -60,6 +60,129 @@ typedef HRESULT (WINAPI *PFN_CREATE_DXGI_FACTORY)(REFIID riid, void **ppFactory) +@@ -60,6 +61,128 @@ typedef HRESULT (WINAPI *PFN_CREATE_DXGI_FACTORY)(REFIID riid, void **ppFactory) #include "pixdesc.h" #include "pixfmt.h" @@ -1237,8 +1167,7 @@ index 95a68e62c5..0e42a36346 100644 + + // Optional functions + funcs->vaSyncBuffer = dlsym(funcs->handle_va, "vaSyncBuffer"); -+ av_log(NULL, AV_LOG_ERROR, "vaSyncBuffer:%p.\n", funcs->vaSyncBuffer); // use error log level to print it out -+ ++ av_log(NULL, AV_LOG_DEBUG, "vaSyncBuffer:%p.\n", funcs->vaSyncBuffer); + + return funcs; + @@ -1457,7 +1386,7 @@ index 95a68e62c5..0e42a36346 100644 VAAPIFramesContext *ctx = hwfc->hwctx; VASurfaceID surface_id; const VAAPIFormatDescriptor *desc; -@@ -836,10 +966,10 @@ static int vaapi_map_frame(AVHWFramesContext *hwfc, +@@ -839,10 +969,10 @@ static int vaapi_map_frame(AVHWFramesContext *hwfc, map->flags = flags; map->image.image_id = VA_INVALID_ID; @@ -1470,7 +1399,7 @@ index 95a68e62c5..0e42a36346 100644 err = AVERROR(EIO); goto fail; } -@@ -853,11 +983,11 @@ static int vaapi_map_frame(AVHWFramesContext *hwfc, +@@ -856,11 +986,11 @@ static int vaapi_map_frame(AVHWFramesContext *hwfc, // prefer not to be given direct-mapped memory if they request read access. if (ctx->derive_works && dst->format == hwfc->sw_format && ((flags & AV_HWFRAME_MAP_DIRECT) || !(flags & AV_HWFRAME_MAP_READ))) { @@ -1484,7 +1413,7 @@ index 95a68e62c5..0e42a36346 100644 err = AVERROR(EIO); goto fail; } -@@ -870,32 +1000,32 @@ static int vaapi_map_frame(AVHWFramesContext *hwfc, +@@ -873,41 +1003,32 @@ static int vaapi_map_frame(AVHWFramesContext *hwfc, } map->flags |= AV_HWFRAME_MAP_DIRECT; } else { @@ -1514,7 +1443,16 @@ index 95a68e62c5..0e42a36346 100644 } } +-#if VA_CHECK_VERSION(1, 21, 0) +- if (flags & AV_HWFRAME_MAP_READ) +- vaflags |= VA_MAPBUFFER_FLAG_READ; +- if (flags & AV_HWFRAME_MAP_WRITE) +- vaflags |= VA_MAPBUFFER_FLAG_WRITE; +- // On drivers not implementing vaMapBuffer2 libva calls vaMapBuffer instead. +- vas = vaMapBuffer2(hwctx->display, map->image.buf, &address, vaflags); +-#else - vas = vaMapBuffer(hwctx->display, map->image.buf, &address); +-#endif + vas = vaf->vaMapBuffer(hwctx->display, map->image.buf, &address); if (vas != VA_STATUS_SUCCESS) { av_log(hwfc, AV_LOG_ERROR, "Failed to map image from surface " @@ -1523,7 +1461,7 @@ index 95a68e62c5..0e42a36346 100644 err = AVERROR(EIO); goto fail; } -@@ -924,9 +1054,9 @@ static int vaapi_map_frame(AVHWFramesContext *hwfc, +@@ -936,9 +1057,9 @@ static int vaapi_map_frame(AVHWFramesContext *hwfc, fail: if (map) { if (address) @@ -1535,7 +1473,7 @@ index 95a68e62c5..0e42a36346 100644 av_free(map); } return err; -@@ -1068,12 +1198,12 @@ static void vaapi_unmap_from_drm(AVHWFramesContext *dst_fc, +@@ -1080,12 +1201,12 @@ static void vaapi_unmap_from_drm(AVHWFramesContext *dst_fc, HWMapDescriptor *hwmap) { AVVAAPIDeviceContext *dst_dev = dst_fc->device_ctx->hwctx; @@ -1550,7 +1488,7 @@ index 95a68e62c5..0e42a36346 100644 } static int vaapi_map_from_drm(AVHWFramesContext *src_fc, AVFrame *dst, -@@ -1088,6 +1218,7 @@ static int vaapi_map_from_drm(AVHWFramesContext *src_fc, AVFrame *dst, +@@ -1100,6 +1221,7 @@ static int vaapi_map_from_drm(AVHWFramesContext *src_fc, AVFrame *dst, AVHWFramesContext *dst_fc = (AVHWFramesContext*)dst->hw_frames_ctx->data; AVVAAPIDeviceContext *dst_dev = dst_fc->device_ctx->hwctx; @@ -1558,7 +1496,7 @@ index 95a68e62c5..0e42a36346 100644 const AVDRMFrameDescriptor *desc; const VAAPIFormatDescriptor *format_desc; VASurfaceID surface_id; -@@ -1204,7 +1335,7 @@ static int vaapi_map_from_drm(AVHWFramesContext *src_fc, AVFrame *dst, +@@ -1216,7 +1338,7 @@ static int vaapi_map_from_drm(AVHWFramesContext *src_fc, AVFrame *dst, * Gallium seem to do the correct error checks, so lets just try the * PRIME_2 import first. */ @@ -1567,7 +1505,7 @@ index 95a68e62c5..0e42a36346 100644 src->width, src->height, &surface_id, 1, prime_attrs, FF_ARRAY_ELEMS(prime_attrs)); if (vas != VA_STATUS_SUCCESS) -@@ -1255,7 +1386,7 @@ static int vaapi_map_from_drm(AVHWFramesContext *src_fc, AVFrame *dst, +@@ -1267,7 +1389,7 @@ static int vaapi_map_from_drm(AVHWFramesContext *src_fc, AVFrame *dst, FFSWAP(uint32_t, buffer_desc.offsets[1], buffer_desc.offsets[2]); } @@ -1576,7 +1514,7 @@ index 95a68e62c5..0e42a36346 100644 src->width, src->height, &surface_id, 1, buffer_attrs, FF_ARRAY_ELEMS(buffer_attrs)); -@@ -1286,14 +1417,14 @@ static int vaapi_map_from_drm(AVHWFramesContext *src_fc, AVFrame *dst, +@@ -1298,14 +1420,14 @@ static int vaapi_map_from_drm(AVHWFramesContext *src_fc, AVFrame *dst, FFSWAP(uint32_t, buffer_desc.offsets[1], buffer_desc.offsets[2]); } @@ -1593,7 +1531,7 @@ index 95a68e62c5..0e42a36346 100644 return AVERROR(EIO); } av_log(dst_fc, AV_LOG_DEBUG, "Create surface %#x.\n", surface_id); -@@ -1331,6 +1462,7 @@ static int vaapi_map_to_drm_esh(AVHWFramesContext *hwfc, AVFrame *dst, +@@ -1343,6 +1465,7 @@ static int vaapi_map_to_drm_esh(AVHWFramesContext *hwfc, AVFrame *dst, const AVFrame *src, int flags) { AVVAAPIDeviceContext *hwctx = hwfc->device_ctx->hwctx; @@ -1601,7 +1539,7 @@ index 95a68e62c5..0e42a36346 100644 VASurfaceID surface_id; VAStatus vas; VADRMPRIMESurfaceDescriptor va_desc; -@@ -1344,10 +1476,10 @@ static int vaapi_map_to_drm_esh(AVHWFramesContext *hwfc, AVFrame *dst, +@@ -1356,10 +1479,10 @@ static int vaapi_map_to_drm_esh(AVHWFramesContext *hwfc, AVFrame *dst, if (flags & AV_HWFRAME_MAP_READ) { export_flags |= VA_EXPORT_SURFACE_READ_ONLY; @@ -1614,7 +1552,7 @@ index 95a68e62c5..0e42a36346 100644 return AVERROR(EIO); } } -@@ -1355,14 +1487,14 @@ static int vaapi_map_to_drm_esh(AVHWFramesContext *hwfc, AVFrame *dst, +@@ -1367,14 +1490,14 @@ static int vaapi_map_to_drm_esh(AVHWFramesContext *hwfc, AVFrame *dst, if (flags & AV_HWFRAME_MAP_WRITE) export_flags |= VA_EXPORT_SURFACE_WRITE_ONLY; @@ -1631,7 +1569,7 @@ index 95a68e62c5..0e42a36346 100644 return AVERROR(EIO); } -@@ -1425,6 +1557,7 @@ static void vaapi_unmap_to_drm_abh(AVHWFramesContext *hwfc, +@@ -1437,6 +1560,7 @@ static void vaapi_unmap_to_drm_abh(AVHWFramesContext *hwfc, HWMapDescriptor *hwmap) { AVVAAPIDeviceContext *hwctx = hwfc->device_ctx->hwctx; @@ -1639,7 +1577,7 @@ index 95a68e62c5..0e42a36346 100644 VAAPIDRMImageBufferMapping *mapping = hwmap->priv; VASurfaceID surface_id; VAStatus vas; -@@ -1436,19 +1569,19 @@ static void vaapi_unmap_to_drm_abh(AVHWFramesContext *hwfc, +@@ -1448,19 +1572,19 @@ static void vaapi_unmap_to_drm_abh(AVHWFramesContext *hwfc, // DRM PRIME file descriptors are closed by vaReleaseBufferHandle(), // so we shouldn't close them separately. @@ -1663,7 +1601,7 @@ index 95a68e62c5..0e42a36346 100644 } av_free(mapping); -@@ -1458,6 +1591,7 @@ static int vaapi_map_to_drm_abh(AVHWFramesContext *hwfc, AVFrame *dst, +@@ -1470,6 +1594,7 @@ static int vaapi_map_to_drm_abh(AVHWFramesContext *hwfc, AVFrame *dst, const AVFrame *src, int flags) { AVVAAPIDeviceContext *hwctx = hwfc->device_ctx->hwctx; @@ -1671,7 +1609,7 @@ index 95a68e62c5..0e42a36346 100644 VAAPIDRMImageBufferMapping *mapping = NULL; VASurfaceID surface_id; VAStatus vas; -@@ -1471,12 +1605,12 @@ static int vaapi_map_to_drm_abh(AVHWFramesContext *hwfc, AVFrame *dst, +@@ -1483,12 +1608,12 @@ static int vaapi_map_to_drm_abh(AVHWFramesContext *hwfc, AVFrame *dst, if (!mapping) return AVERROR(ENOMEM); @@ -1686,7 +1624,7 @@ index 95a68e62c5..0e42a36346 100644 err = AVERROR(EIO); goto fail; } -@@ -1531,13 +1665,13 @@ static int vaapi_map_to_drm_abh(AVHWFramesContext *hwfc, AVFrame *dst, +@@ -1543,13 +1668,13 @@ static int vaapi_map_to_drm_abh(AVHWFramesContext *hwfc, AVFrame *dst, } } @@ -1702,7 +1640,7 @@ index 95a68e62c5..0e42a36346 100644 err = AVERROR(EIO); goto fail_derived; } -@@ -1566,9 +1700,9 @@ static int vaapi_map_to_drm_abh(AVHWFramesContext *hwfc, AVFrame *dst, +@@ -1578,9 +1703,9 @@ static int vaapi_map_to_drm_abh(AVHWFramesContext *hwfc, AVFrame *dst, return 0; fail_mapped: @@ -1714,17 +1652,16 @@ index 95a68e62c5..0e42a36346 100644 fail: av_freep(&mapping); return err; -@@ -1622,9 +1756,16 @@ static void vaapi_device_free(AVHWDeviceContext *ctx) +@@ -1634,9 +1759,15 @@ static void vaapi_device_free(AVHWDeviceContext *ctx) { AVVAAPIDeviceContext *hwctx = ctx->hwctx; VAAPIDevicePriv *priv = ctx->user_opaque; + VAAPIDynLoadFunctions *vaf = hwctx->funcs; -+ -+ if (hwctx && hwctx->display && vaf && vaf->vaTerminate) -+ vaf->vaTerminate(hwctx->display); - if (hwctx->display) - vaTerminate(hwctx->display); ++ if (hwctx && hwctx->display && vaf && vaf->vaTerminate) ++ vaf->vaTerminate(hwctx->display); + + if (hwctx && hwctx->funcs) { + vaapi_free_functions(hwctx->funcs); @@ -1733,7 +1670,7 @@ index 95a68e62c5..0e42a36346 100644 #if HAVE_VAAPI_X11 if (priv->x11_display) -@@ -1657,20 +1798,21 @@ static int vaapi_device_connect(AVHWDeviceContext *ctx, +@@ -1669,20 +1800,21 @@ static int vaapi_device_connect(AVHWDeviceContext *ctx, VADisplay display) { AVVAAPIDeviceContext *hwctx = ctx->hwctx; @@ -1759,7 +1696,7 @@ index 95a68e62c5..0e42a36346 100644 return AVERROR(EIO); } av_log(ctx, AV_LOG_VERBOSE, "Initialised VAAPI connection: " -@@ -1686,6 +1828,16 @@ static int vaapi_device_create(AVHWDeviceContext *ctx, const char *device, +@@ -1698,6 +1830,16 @@ static int vaapi_device_create(AVHWDeviceContext *ctx, const char *device, VADisplay display = NULL; const AVDictionaryEntry *ent; int try_drm, try_x11, try_win32, try_all; @@ -1776,7 +1713,7 @@ index 95a68e62c5..0e42a36346 100644 priv = av_mallocz(sizeof(*priv)); if (!priv) -@@ -1802,7 +1954,7 @@ static int vaapi_device_create(AVHWDeviceContext *ctx, const char *device, +@@ -1843,7 +1985,7 @@ static int vaapi_device_create(AVHWDeviceContext *ctx, const char *device, break; } @@ -1785,7 +1722,7 @@ index 95a68e62c5..0e42a36346 100644 if (!display) { av_log(ctx, AV_LOG_VERBOSE, "Cannot open a VA display " "from DRM device %s.\n", device); -@@ -1820,7 +1972,7 @@ static int vaapi_device_create(AVHWDeviceContext *ctx, const char *device, +@@ -1861,7 +2003,7 @@ static int vaapi_device_create(AVHWDeviceContext *ctx, const char *device, av_log(ctx, AV_LOG_VERBOSE, "Cannot open X11 display " "%s.\n", XDisplayName(device)); } else { @@ -1794,7 +1731,7 @@ index 95a68e62c5..0e42a36346 100644 if (!display) { av_log(ctx, AV_LOG_ERROR, "Cannot open a VA display " "from X11 display %s.\n", XDisplayName(device)); -@@ -1909,11 +2061,11 @@ static int vaapi_device_create(AVHWDeviceContext *ctx, const char *device, +@@ -1950,11 +2092,11 @@ static int vaapi_device_create(AVHWDeviceContext *ctx, const char *device, if (ent) { #if VA_CHECK_VERSION(0, 38, 0) VAStatus vas; @@ -1809,7 +1746,7 @@ index 95a68e62c5..0e42a36346 100644 return AVERROR_EXTERNAL; } #else -@@ -1929,6 +2081,8 @@ static int vaapi_device_derive(AVHWDeviceContext *ctx, +@@ -1970,6 +2112,8 @@ static int vaapi_device_derive(AVHWDeviceContext *ctx, AVHWDeviceContext *src_ctx, AVDictionary *opts, int flags) { @@ -1818,7 +1755,7 @@ index 95a68e62c5..0e42a36346 100644 #if HAVE_VAAPI_DRM if (src_ctx->type == AV_HWDEVICE_TYPE_DRM) { AVDRMDeviceContext *src_hwctx = src_ctx->hwctx; -@@ -2000,7 +2154,7 @@ static int vaapi_device_derive(AVHWDeviceContext *ctx, +@@ -2041,7 +2185,7 @@ static int vaapi_device_derive(AVHWDeviceContext *ctx, ctx->user_opaque = priv; ctx->free = &vaapi_device_free; @@ -1827,21 +1764,8 @@ index 95a68e62c5..0e42a36346 100644 if (!display) { av_log(ctx, AV_LOG_ERROR, "Failed to open a VA display from " "DRM device.\n"); -@@ -2010,6 +2164,7 @@ static int vaapi_device_derive(AVHWDeviceContext *ctx, - return vaapi_device_connect(ctx, display); - } - #endif -+ - return AVERROR(ENOSYS); - } - -@@ -2040,3 +2195,4 @@ const HWContextType ff_hwcontext_type_vaapi = { - AV_PIX_FMT_NONE - }, - }; -+ diff --git a/libavutil/hwcontext_vaapi.h b/libavutil/hwcontext_vaapi.h -index 0b2e071cb3..7bdb21c66a 100644 +index 0b2e071cb3..2c51223d45 100644 --- a/libavutil/hwcontext_vaapi.h +++ b/libavutil/hwcontext_vaapi.h @@ -20,6 +20,100 @@ @@ -1954,40 +1878,6 @@ index 0b2e071cb3..7bdb21c66a 100644 } AVVAAPIDeviceContext; /** -@@ -114,4 +210,5 @@ typedef struct AVVAAPIHWConfig { - VAConfigID config_id; - } AVVAAPIHWConfig; - -+ - #endif /* AVUTIL_HWCONTEXT_VAAPI_H */ -diff --git a/libavutil/hwcontext_vulkan.c b/libavutil/hwcontext_vulkan.c -index 6e3b96b73a..55ba57ea7d 100644 ---- a/libavutil/hwcontext_vulkan.c -+++ b/libavutil/hwcontext_vulkan.c -@@ -1597,6 +1597,7 @@ static int vulkan_device_derive(AVHWDeviceContext *ctx, - #if CONFIG_VAAPI - case AV_HWDEVICE_TYPE_VAAPI: { - AVVAAPIDeviceContext *src_hwctx = src_ctx->hwctx; -+ VAAPIDynLoadFunctions *vaf = src_hwctx->funcs; - VADisplay dpy = src_hwctx->display; - #if VA_CHECK_VERSION(1, 15, 0) - VAStatus vas; -@@ -1607,13 +1608,13 @@ static int vulkan_device_derive(AVHWDeviceContext *ctx, - const char *vendor; - - #if VA_CHECK_VERSION(1, 15, 0) -- vas = vaGetDisplayAttributes(dpy, &attr, 1); -+ vas = vaf->vaGetDisplayAttributes(dpy, &attr, 1); - if (vas == VA_STATUS_SUCCESS && attr.flags != VA_DISPLAY_ATTRIB_NOT_SUPPORTED) - dev_select.pci_device = (attr.value & 0xFFFF); - #endif - - if (!dev_select.pci_device) { -- vendor = vaQueryVendorString(dpy); -+ vendor = vaf->vaQueryVendorString(dpy); - if (!vendor) { - av_log(ctx, AV_LOG_ERROR, "Unable to get device info from VAAPI!\n"); - return AVERROR_EXTERNAL; -- 2.34.1 diff --git a/res/vcpkg/ffmpeg/patch/0007-fix-linux-configure.patch b/res/vcpkg/ffmpeg/patch/0007-fix-linux-configure.patch new file mode 100644 index 000000000000..21a1f4d4fe73 --- /dev/null +++ b/res/vcpkg/ffmpeg/patch/0007-fix-linux-configure.patch @@ -0,0 +1,30 @@ +From 595f0468e127f204741b6c37a479d71daaf571eb Mon Sep 17 00:00:00 2001 +From: 21pages +Date: Tue, 10 Dec 2024 21:17:14 +0800 +Subject: [PATCH] fix linux configure + +Signed-off-by: 21pages +--- + configure | 6 ------ + 1 file changed, 6 deletions(-) + +diff --git a/configure b/configure +index d77a55b653..48ca90ac5e 100755 +--- a/configure ++++ b/configure +@@ -7071,12 +7071,6 @@ enabled mmal && { check_lib mmal interface/mmal/mmal.h mmal_port_co + check_lib mmal interface/mmal/mmal.h mmal_port_connect -lmmal_core -lmmal_util -lmmal_vc_client -lbcm_host; } || + die "ERROR: mmal not found" && + check_func_headers interface/mmal/mmal.h "MMAL_PARAMETER_VIDEO_MAX_NUM_CALLBACKS"; } +-enabled openal && { check_pkg_config openal "openal >= 1.1" "AL/al.h" alGetError || +- { for al_extralibs in "${OPENAL_LIBS}" "-lopenal" "-lOpenAL32"; do +- check_lib openal 'AL/al.h' alGetError "${al_extralibs}" && break; done } || +- die "ERROR: openal not found"; } && +- { test_cpp_condition "AL/al.h" "defined(AL_VERSION_1_1)" || +- die "ERROR: openal must be installed and version must be 1.1 or compatible"; } + enabled opencl && { check_pkg_config opencl OpenCL CL/cl.h clEnqueueNDRangeKernel || + check_lib opencl OpenCL/cl.h clEnqueueNDRangeKernel "-framework OpenCL" || + check_lib opencl CL/cl.h clEnqueueNDRangeKernel -lOpenCL || +-- +2.34.1 + diff --git a/res/vcpkg/ffmpeg/portfile.cmake b/res/vcpkg/ffmpeg/portfile.cmake index 0e35a9550c04..e7a530ee6465 100644 --- a/res/vcpkg/ffmpeg/portfile.cmake +++ b/res/vcpkg/ffmpeg/portfile.cmake @@ -2,20 +2,27 @@ vcpkg_from_github( OUT_SOURCE_PATH SOURCE_PATH REPO ffmpeg/ffmpeg REF "n${VERSION}" - SHA512 3ba02e8b979c80bf61d55f414bdac2c756578bb36498ed7486151755c6ccf8bd8ff2b8c7afa3c5d1acd862ce48314886a86a105613c05e36601984c334f8f6bf + SHA512 3b273769ef1a1b63aed0691eef317a760f8c83b1d0e1c232b67bbee26db60b4864aafbc88df0e86d6bebf07185bbd057f33e2d5258fde6d97763b9994cd48b6f HEAD_REF master PATCHES - 0002-fix-msvc-link.patch # upstreamed in future version + 0001-create-lib-libraries.patch + 0002-fix-msvc-link.patch 0003-fix-windowsinclude.patch - 0005-fix-nasm.patch # upstreamed in future version - 0012-Fix-ssl-110-detection.patch + 0004-dependencies.patch + 0005-fix-nasm.patch + 0007-fix-lib-naming.patch 0013-define-WINVER.patch + 0020-fix-aarch64-libswscale.patch + 0024-fix-osx-host-c11.patch + 0040-ffmpeg-add-av_stream_get_first_dts-for-chromium.patch # Do not remove this patch. It is required by chromium + 0041-add-const-for-opengl-definition.patch + 0043-fix-miss-head.patch patch/0001-avcodec-amfenc-add-query_timeout-option-for-h264-hev.patch patch/0002-libavcodec-amfenc-reconfig-when-bitrate-change.patch - patch/0003-amf-colorspace.patch patch/0004-videotoolbox-changing-bitrate.patch patch/0005-mediacodec-changing-bitrate.patch patch/0006-dlopen-libva.patch + patch/0007-fix-linux-configure.patch ) if(SOURCE_PATH MATCHES " ") @@ -51,6 +58,7 @@ set(OPTIONS "\ --disable-debug \ --disable-valgrind-backtrace \ --disable-large-tests \ +--disable-bzlib \ --disable-avdevice \ --enable-avcodec \ --enable-avformat \ diff --git a/res/vcpkg/ffmpeg/vcpkg.json b/res/vcpkg/ffmpeg/vcpkg.json index f7612d9281c2..0346bb585763 100644 --- a/res/vcpkg/ffmpeg/vcpkg.json +++ b/res/vcpkg/ffmpeg/vcpkg.json @@ -1,7 +1,7 @@ { "name": "ffmpeg", - "version": "7.0.2", - "port-version": 0, + "version": "7.1", + "port-version": 1, "description": [ "a library to decode, encode, transcode, mux, demux, stream, filter and play pretty much anything that humans and machines have created.", "FFmpeg is the leading multimedia framework, able to decode, encode, transcode, mux, demux, stream, filter and play pretty much anything that humans and machines have created. It supports the most obscure ancient formats up to the cutting edge. No matter if they were designed by some standards committee, the community or a corporation. It is also highly portable: FFmpeg compiles, runs, and passes our testing infrastructure FATE across Linux, Mac OS X, Microsoft Windows, the BSDs, Solaris, etc. under a wide variety of build environments, machine architectures, and configurations." diff --git a/res/vcpkg/libvpx/portfile.cmake b/res/vcpkg/libvpx/portfile.cmake index 96eab8717390..ac54eafd4c92 100644 --- a/res/vcpkg/libvpx/portfile.cmake +++ b/res/vcpkg/libvpx/portfile.cmake @@ -4,7 +4,7 @@ vcpkg_from_github( OUT_SOURCE_PATH SOURCE_PATH REPO webmproject/libvpx REF "v${VERSION}" - SHA512 3e3bfad3d035c0bc3db7cb5a194d56d3c90f5963fb1ad527ae5252054e7c48ce2973de1346c97d94b59f7a95d4801bec44214cce10faf123f92b36fca79a8d1e + SHA512 8f483653a324c710fd431b87fd0d5d6f476f006bd8c8e9c6d1fa6abd105d6a40ac81c8fd5638b431c455d57ab2ee823c165e9875eb3932e6e518477422da3a7b HEAD_REF master PATCHES 0002-Fix-nasm-debug-format-flag.patch diff --git a/res/vcpkg/libvpx/vcpkg.json b/res/vcpkg/libvpx/vcpkg.json index ca4a47d309bb..d19c5daca068 100644 --- a/res/vcpkg/libvpx/vcpkg.json +++ b/res/vcpkg/libvpx/vcpkg.json @@ -1,6 +1,6 @@ { "name": "libvpx", - "version": "1.14.1", + "version": "1.15.0", "port-version": 0, "description": "The reference software implementation for the video coding formats VP8 and VP9.", "homepage": "https://github.com/webmproject/libvpx", diff --git a/vcpkg.json b/vcpkg.json index 75cee85b1502..0b2b9ab4f9e7 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -89,7 +89,7 @@ }, { "name": "amd-amf", - "version": "1.4.29" + "version": "1.4.35" }, { "name": "mfx-dispatch", From c06e1d74b4892770eeddc0259d7728ebc9326407 Mon Sep 17 00:00:00 2001 From: Dmitry Beskov <43372966+besdar@users.noreply.github.com> Date: Sun, 15 Dec 2024 12:55:43 +0400 Subject: [PATCH 475/541] changes for flatpak build (#10273) --- .github/workflows/flutter-build.yml | 25 ++------- flatpak/com.rustdesk.RustDesk.metainfo.xml | 59 ++++++++++++++++++++++ flatpak/rustdesk.json | 47 +++++++++-------- flatpak/xdotool.json | 15 ------ 4 files changed, 87 insertions(+), 59 deletions(-) create mode 100644 flatpak/com.rustdesk.RustDesk.metainfo.xml delete mode 100644 flatpak/xdotool.json diff --git a/.github/workflows/flutter-build.yml b/.github/workflows/flutter-build.yml index 1b6dbf1cdc99..f9e633348c8d 100644 --- a/.github/workflows/flutter-build.yml +++ b/.github/workflows/flutter-build.yml @@ -2011,36 +2011,17 @@ jobs: shell: /bin/bash install: | apt-get update -y - apt-get install -y \ - curl \ - git \ - rpm \ - wget + apt-get install -y git flatpak flatpak-builder run: | # disable git safe.directory git config --global --add safe.directory "*" pushd /workspace - # install - apt-get update -y - apt-get install -y \ - cmake \ - curl \ - flatpak \ - flatpak-builder \ - gcc \ - git \ - g++ \ - libgtk-3-dev \ - nasm \ - wget # flatpak deps - flatpak --user remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo - flatpak --user install -y flathub org.freedesktop.Platform/${{ matrix.job.arch }}/23.08 - flatpak --user install -y flathub org.freedesktop.Sdk/${{ matrix.job.arch }}/23.08 + flatpak --user remote-add --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo # package pushd flatpak git clone https://github.com/flathub/shared-modules.git --depth=1 - flatpak-builder --user --force-clean --repo=repo ./build ./rustdesk.json + flatpak-builder --user --install-deps-from=flathub -y --force-clean --repo=repo ./build ./rustdesk.json flatpak build-bundle ./repo rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}${{ matrix.job.suffix }}.flatpak com.rustdesk.RustDesk - name: Publish flatpak package diff --git a/flatpak/com.rustdesk.RustDesk.metainfo.xml b/flatpak/com.rustdesk.RustDesk.metainfo.xml new file mode 100644 index 000000000000..0d3b33bb8c3d --- /dev/null +++ b/flatpak/com.rustdesk.RustDesk.metainfo.xml @@ -0,0 +1,59 @@ + + + com.rustdesk.RustDesk + + RustDesk + + com.rustdesk.RustDesk.desktop + CC0-1.0 + AGPL-3.0-only + RustDesk + Secure remote desktop access + +

    + RustDesk is a full-featured open source remote control alternative for self-hosting and security with minimal configuration. +

    +
      +
    • Works on Windows, macOS, Linux, iOS, Android, Web.
    • +
    • Supports VP8 / VP9 / AV1 software codecs, and H264 / H265 hardware codecs.
    • +
    • Own your data, easily set up self-hosting solution on your infrastructure.
    • +
    • P2P connection with end-to-end encryption based on NaCl.
    • +
    • No administrative privileges or installation needed for Windows, elevate priviledge locally or from remote on demand.
    • +
    • We like to keep things simple and will strive to make simpler where possible.
    • +
    +

    + For self-hosting setup instructions please go to our home page. +

    +
    + + Utility + + + + Remote desktop session + https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png + + + + #d9eaf8 + #0160ee + + https://rustdesk.com + https://github.com/rustdesk/rustdesk/issues + https://github.com/rustdesk/rustdesk/wiki/FAQ + https://rustdesk.com/docs + https://ko-fi.com/rustdesk + https://github.com/rustdesk/rustdesk + https://github.com/rustdesk/rustdesk/tree/master/src/lang + https://github.com/rustdesk/rustdesk/blob/master/docs/CONTRIBUTING.md + https://rustdesk.com/docs/en/technical-support + + 600 + always + + + keyboard + pointing + + +
    \ No newline at end of file diff --git a/flatpak/rustdesk.json b/flatpak/rustdesk.json index 6d7acb5b89ca..57a2f158ad49 100644 --- a/flatpak/rustdesk.json +++ b/flatpak/rustdesk.json @@ -1,19 +1,30 @@ { "id": "com.rustdesk.RustDesk", "runtime": "org.freedesktop.Platform", - "runtime-version": "23.08", + "runtime-version": "24.08", "sdk": "org.freedesktop.Sdk", "command": "rustdesk", - "icon": "share/icons/hicolor/scalable/apps/rustdesk.svg", + "cleanup": ["/include", "/lib/pkgconfig", "/share/gtk-doc"], + "rename-desktop-file": "rustdesk.desktop", + "rename-icon": "rustdesk", "modules": [ "shared-modules/libappindicator/libappindicator-gtk3-12.10.json", - "xdotool.json", { - "name": "pam", - "buildsystem": "simple", - "build-commands": [ - "./configure --disable-selinux --prefix=/app && make -j4 install" - ], + "name": "xdotool", + "no-autogen": true, + "make-install-args": ["PREFIX=${FLATPAK_DEST}"], + "sources": [ + { + "type": "archive", + "url": "https://github.com/jordansissel/xdotool/releases/download/v3.20211022.1/xdotool-3.20211022.1.tar.gz", + "sha256": "96f0facfde6d78eacad35b91b0f46fecd0b35e474c03e00e30da3fdd345f9ada" + } + ] + }, + { + "name": "pam", + "buildsystem": "autotools", + "config-opts": ["--disable-selinux"], "sources": [ { "type": "archive", @@ -26,32 +37,24 @@ "name": "rustdesk", "buildsystem": "simple", "build-commands": [ - "bsdtar -zxvf rustdesk.deb", - "tar -xvf ./data.tar.xz", - "cp -r ./usr/* /app/", - "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-link.desktop /app/share/applications/com.rustdesk.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", - "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" + "bsdtar -Oxf rustdesk.deb data.tar.xz | bsdtar -xf -", + "cp -r usr/* /app/", + "mkdir -p /app/bin && ln -s /app/lib/rustdesk/rustdesk /app/bin/rustdesk" ], - "cleanup": ["/include", "/lib/pkgconfig", "/share/gtk-doc"], "sources": [ { "type": "file", - "path": "./rustdesk.deb" + "path": "rustdesk.deb" }, { "type": "file", - "path": "../res/scalable.svg" + "path": "com.rustdesk.RustDesk.metainfo.xml" } ] } ], "finish-args": [ "--share=ipc", - "--socket=x11", "--socket=fallback-x11", "--socket=wayland", "--share=network", @@ -60,4 +63,4 @@ "--socket=pulseaudio", "--talk-name=org.freedesktop.Flatpak" ] -} +} \ No newline at end of file diff --git a/flatpak/xdotool.json b/flatpak/xdotool.json deleted file mode 100644 index d7f41bf0ec09..000000000000 --- a/flatpak/xdotool.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "name": "xdotool", - "buildsystem": "simple", - "build-commands": [ - "make -j4 && PREFIX=./build make install", - "cp -r ./build/* /app/" - ], - "sources": [ - { - "type": "archive", - "url": "https://github.com/jordansissel/xdotool/releases/download/v3.20211022.1/xdotool-3.20211022.1.tar.gz", - "sha256": "96f0facfde6d78eacad35b91b0f46fecd0b35e474c03e00e30da3fdd345f9ada" - } - ] -} From db3bdb16a1c30cf41559bd038b3f69edf01f3974 Mon Sep 17 00:00:00 2001 From: summoner001 Date: Mon, 16 Dec 2024 08:48:17 +0100 Subject: [PATCH 476/541] Update hu.rs (#10287) * Update hu.rs Fixes and corrections * Update hu.rs more fixes * Update hu.rs Minor fixes * Update hu.rs Fixing typo --- src/lang/hu.rs | 324 ++++++++++++++++++++++++------------------------- 1 file changed, 162 insertions(+), 162 deletions(-) diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 8180b9f5f307..7b149938e1aa 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -7,88 +7,88 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Password", "Jelszó"), ("Ready", "Kész"), ("Established", "Létrejött"), - ("connecting_status", "Csatlakozás folyamatban..."), + ("connecting_status", "Kapcsolódás folyamatban…"), ("Enable service", "Szolgáltatás engedélyezése"), ("Start service", "Szolgáltatás indítása"), ("Service is running", "Szolgáltatás aktív"), ("Service is not running", "Szolgáltatás inaktív"), - ("not_ready_status", "Kapcsolódási hiba. Kérlek ellenőrizze a hálózati beállításokat."), + ("not_ready_status", "Kapcsolódási hiba. Ellenőrizze a hálózati beállításokat."), ("Control Remote Desktop", "Távoli számítógép vezérlése"), ("Transfer file", "Fájlátvitel"), - ("Connect", "Csatlakozás"), - ("Recent sessions", "Legutóbbi munkamanetek"), + ("Connect", "Kapcsolódás"), + ("Recent sessions", "Legutóbbi munkamenetek"), ("Address book", "Címjegyzék"), ("Confirmation", "Megerősítés"), - ("TCP tunneling", "TCP alagútépítés"), - ("Remove", "Eltávolít"), + ("TCP tunneling", "TCP-tunneling"), + ("Remove", "Eltávolítás"), ("Refresh random password", "Új véletlenszerű jelszó"), ("Set your own password", "Saját jelszó beállítása"), ("Enable keyboard/mouse", "Billentyűzet/egér engedélyezése"), ("Enable clipboard", "Megosztott vágólap engedélyezése"), ("Enable file transfer", "Fájlátvitel engedélyezése"), - ("Enable TCP tunneling", "TCP alagútépítés engedélyezése"), + ("Enable TCP tunneling", "TCP-tunneling engedélyezése"), ("IP Whitelisting", "IP engedélyezési lista"), - ("ID/Relay Server", "ID/Továbbító szerver"), - ("Import server config", "Szerver konfiguráció importálása"), - ("Export Server Config", "Szerver konfiguráció exportálása"), - ("Import server configuration successfully", "Szerver konfiguráció sikeresen importálva"), - ("Export server configuration successfully", "Szerver konfiguráció sikeresen exportálva"), - ("Invalid server configuration", "Érvénytelen szerver konfiguráció"), + ("ID/Relay Server", "ID/Továbbító-kiszolgáló"), + ("Import server config", "Kiszolgáló-konfiguráció importálása"), + ("Export Server Config", "Kiszolgáló-konfiguráció exportálása"), + ("Import server configuration successfully", "Kiszolgáló-konfiguráció sikeresen importálva"), + ("Export server configuration successfully", "Kiszolgáló-konfiguráció sikeresen exportálva"), + ("Invalid server configuration", "Érvénytelen kiszolgáló-konfiguráció"), ("Clipboard is empty", "A vágólap üres"), ("Stop service", "Szolgáltatás leállítása"), ("Change ID", "Azonosító megváltoztatása"), - ("Your new ID", "Az új azonosítód"), + ("Your new ID", "Az új azonosítója"), ("length %min% to %max%", "hossz %min% és %max% között"), ("starts with a letter", "betűvel kezdődik"), ("allowed characters", "engedélyezett karakterek"), ("id_change_tip", "Csak a-z, A-Z, 0-9 csoportokba tartozó karakterek, illetve a _ karakter van engedélyezve. Az első karakternek mindenképpen a-z, A-Z csoportokba kell esnie. Az azonosító hosszúsága 6-tól, 16 karakter."), ("Website", "Webhely"), - ("About", "Rólunk"), + ("About", "Névjegy"), ("Slogan_tip", "Szenvedéllyel programozva - egy káoszba süllyedő világban!"), ("Privacy Statement", "Adatvédelmi nyilatkozat"), ("Mute", "Némítás"), - ("Build Date", "Build ideje"), + ("Build Date", "Összeállítás ideje"), ("Version", "Verzió"), ("Home", "Kezdőképernyő"), ("Audio Input", "Hangátvitel"), ("Enhancements", "Fejlesztések"), - ("Hardware Codec", "Hardveres kódek"), + ("Hardware Codec", "Hardveres kodek"), ("Adaptive bitrate", "Adaptív bitráta"), - ("ID Server", "ID szerver"), - ("Relay Server", "Továbbító szerver"), - ("API Server", "API szerver"), + ("ID Server", "ID kiszolgáló"), + ("Relay Server", "Továbbító-kiszolgáló"), + ("API Server", "API kiszolgáló"), ("invalid_http", "A címnek mindenképpen http(s)://-el kell kezdődnie."), - ("Invalid IP", "A megadott IP cím helytelen."), + ("Invalid IP", "A megadott IP-cím helytelen."), ("Invalid format", "Érvénytelen formátum"), - ("server_not_support", "Nem támogatott a szerver által"), - ("Not available", "Nem elérhető"), + ("server_not_support", "A kiszolgáló nem támogatja"), + ("Not available", "Nem érhető el"), ("Too frequent", "Túl gyakori"), - ("Cancel", "Mégsem"), + ("Cancel", "Mégse"), ("Skip", "Kihagyás"), ("Close", "Bezárás"), ("Retry", "Újra"), ("OK", "OK"), - ("Password Required", "Jelszó megadása kötelező"), - ("Please enter your password", "Kérem írja be a jelszavát"), + ("Password Required", "A jelszó megadása kötelező"), + ("Please enter your password", "Adja meg a jelszavát"), ("Remember password", "Jelszó megjegyzése"), ("Wrong Password", "Hibás jelszó"), ("Do you want to enter again?", "Szeretne újra belépni?"), - ("Connection Error", "Csatlakozási hiba"), + ("Connection Error", "Kapcsolódási hiba"), ("Error", "Hiba"), ("Reset by the peer", "A kapcsolatot alaphelyzetbe állt"), - ("Connecting...", "Csatlakozás..."), - ("Connection in progress. Please wait.", "Csatlakozás folyamatban. Kérem várjon."), - ("Please try 1 minute later", "Kérem próbálja meg 1 perc múlva"), + ("Connecting...", "Kapcsolódás…"), + ("Connection in progress. Please wait.", "A kapcsolódás folyamatban van. Kis türelmet."), + ("Please try 1 minute later", "Próbálja meg 1 perc múlva"), ("Login Error", "Bejelentkezési hiba"), ("Successful", "Sikeres"), - ("Connected, waiting for image...", "Csatlakozva, várakozás a kép adatokra..."), + ("Connected, waiting for image...", "Kapcsolódva, várakozás a képadatokra…"), ("Name", "Név"), ("Type", "Típus"), ("Modified", "Módosított"), ("Size", "Méret"), - ("Show Hidden Files", "Rejtett fájlok mutatása"), - ("Receive", "Fogad"), - ("Send", "Küld"), + ("Show Hidden Files", "Rejtett fájlok megjelenítése"), + ("Receive", "Fogadás"), + ("Send", "Küldés"), ("Refresh File", "Fájl frissítése"), ("Local", "Helyi"), ("Remote", "Távoli"), @@ -99,21 +99,21 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Properties", "Tulajdonságok"), ("Multi Select", "Többszörös kijelölés"), ("Select All", "Összes kijelölése"), - ("Unselect All", "Kijelölések megszűntetése"), + ("Unselect All", "Kijelölések megszüntetése"), ("Empty Directory", "Üres könyvtár"), ("Not an empty directory", "Nem egy üres könyvtár"), ("Are you sure you want to delete this file?", "Biztosan törli ezt a fájlt?"), ("Are you sure you want to delete this empty directory?", "Biztosan törli ezt az üres könyvtárat?"), - ("Are you sure you want to delete the file of this directory?", "Biztos benne, hogy törölni szeretné a könyvtár tartalmát?"), + ("Are you sure you want to delete the file of this directory?", "Biztosan törölni szeretné a könyvtár tartalmát?"), ("Do this for all conflicts", "Tegye ezt minden ütközéskor"), - ("This is irreversible!", "Ez a folyamat visszafordíthatatlan!"), + ("This is irreversible!", "Ez a művelet nem vonható vissza!"), ("Deleting", "Törlés folyamatban"), ("files", "fájlok"), ("Waiting", "Várakozás"), ("Finished", "Befejezve"), ("Speed", "Sebesség"), - ("Custom Image Quality", "Egyedi képminőség"), - ("Privacy mode", "Inkognító mód"), + ("Custom Image Quality", "Egyéni képminőség"), + ("Privacy mode", "Inkognitó mód"), ("Block user input", "Felhasználói bevitel letiltása"), ("Unblock user input", "Felhasználói bevitel engedélyezése"), ("Adjust Window", "Ablakméret beállítása"), @@ -125,7 +125,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Good image quality", "Eredetihez hű"), ("Balanced", "Kiegyensúlyozott"), ("Optimize reaction time", "Gyorsan reagáló"), - ("Custom", "Egyedi"), + ("Custom", "Egyéni"), ("Show remote cursor", "Távoli kurzor megjelenítése"), ("Show quality monitor", "Minőségi monitor megjelenítése"), ("Disable clipboard", "Közös vágólap kikapcsolása"), @@ -134,37 +134,37 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Insert Lock", "Távoli fiók zárolása"), ("Refresh", "Frissítés"), ("ID does not exist", "Az azonosító nem létezik"), - ("Failed to connect to rendezvous server", "Nem sikerült csatlakozni a kiszolgáló szerverhez"), - ("Please try later", "Kérjük, próbálja később"), + ("Failed to connect to rendezvous server", "Nem sikerült kapcsolódni a kiszolgálóhoz"), + ("Please try later", "Próbálja meg később"), ("Remote desktop is offline", "A távoli számítógép offline állapotban van"), ("Key mismatch", "Eltérés a kulcsokban"), ("Timeout", "Időtúllépés"), - ("Failed to connect to relay server", "Nem sikerült csatlakozni a közvetítő szerverhez"), - ("Failed to connect via rendezvous server", "Nem sikerült csatlakozni a kiszolgáló szerveren keresztül"), - ("Failed to connect via relay server", "Nem sikerült csatlakozni a közvetítő szerveren keresztül"), + ("Failed to connect to relay server", "Nem sikerült kapcsolódni a továbbító-kiszolgálóhoz"), + ("Failed to connect via rendezvous server", "Nem sikerült kapcsolódni a kiszolgálón keresztül"), + ("Failed to connect via relay server", "Nem sikerült kapcsolódni a továbbító-kiszolgálón keresztül"), ("Failed to make direct connection to remote desktop", "Nem sikerült közvetlen kapcsolatot létesíteni a távoli számítógéppel"), ("Set Password", "Jelszó beállítása"), ("OS Password", "Operációs rendszer jelszavának beállítása"), - ("install_tip", "Előfordul, hogy bizonyos esetekben hiba léphet fel a Portable verzió használata során. A megfelelő működés érdekében, kérem telepítse a RustDesk alkalmazást a számítógépre."), + ("install_tip", "Előfordul, hogy bizonyos esetekben hiba léphet fel a Portable verzió használata során. A megfelelő működés érdekében, telepítse a RustDesk alkalmazást a számítógépére."), ("Click to upgrade", "Kattintson ide a frissítés telepítéséhez"), ("Click to download", "Kattintson ide a letöltéshez"), ("Click to update", "Kattintson ide a frissítés letöltéséhez"), ("Configure", "Beállítás"), - ("config_acc", "A távoli vezérléshez a RustDesk-nek \"Kisegítő lehetőség\" engedélyre van szüksége"), - ("config_screen", "A távoli vezérléshez szükséges a \"Képernyőfelvétel\" engedély megadása"), - ("Installing ...", "Telepítés..."), - ("Install", "Telepítsd"), + ("config_acc", "A távoli vezérléshez a RustDesknek „Kisegítő lehetőségek” engedélyre van szüksége"), + ("config_screen", "A távoli vezérléshez szükséges a „Képernyőfelvétel” engedély megadása"), + ("Installing ...", "Telepítés…"), + ("Install", "Telepítés"), ("Installation", "Telepítés"), ("Installation Path", "Telepítési útvonal"), ("Create start menu shortcuts", "Start menü parancsikonok létrehozása"), ("Create desktop icon", "Ikon létrehozása az asztalon"), - ("agreement_tip", "A telepítés folytatásával automatikusan elfogadásra kerül a licensz szerződés."), + ("agreement_tip", "A telepítés folytatásával automatikusan elfogadásra kerül a licenc szerződés."), ("Accept and Install", "Elfogadás és telepítés"), - ("End-user license agreement", "Felhasználói licensz szerződés"), - ("Generating ...", "Létrehozás..."), + ("End-user license agreement", "Végfelhasználói licenc szerződés"), + ("Generating ...", "Előállítás…"), ("Your installation is lower version.", "A telepített verzió alacsonyabb."), - ("not_close_tcp_tip", "Ne zárja be ezt az ablakot miközben a tunnelt használja"), - ("Listening ...", "Figyelés..."), + ("not_close_tcp_tip", "Ne zárja be ezt az ablakot amíg TCP-tunnelinget használ"), + ("Listening ...", "Figyelés…"), ("Remote Host", "Távoli kiszolgáló"), ("Remote Port", "Távoli port"), ("Action", "Indítás"), @@ -172,7 +172,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Local Port", "Helyi port"), ("Local Address", "Helyi cím"), ("Change Local Port", "Helyi port megváltoztatása"), - ("setup_server_tip", "Gyorsabb kapcsolat érdekében, hozzon létre saját szervert"), + ("setup_server_tip", "Gyorsabb kapcsolat érdekében, hozzon létre saját kiszolgálót"), ("Too short, at least 6 characters.", "Túl rövid, legalább 6 karakter."), ("The confirmation is not identical.", "A megerősítés nem volt azonos"), ("Permissions", "Engedélyek"), @@ -180,14 +180,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Dismiss", "Elutasítás"), ("Disconnect", "Kapcsolat bontása"), ("Enable file copy and paste", "Fájlok másolásának és beillesztésének engedélyezése"), - ("Connected", "Csatlakozva"), + ("Connected", "Kapcsolódva"), ("Direct and encrypted connection", "Közvetlen, és titkosított kapcsolat"), ("Relayed and encrypted connection", "Továbbított, és titkosított kapcsolat"), ("Direct and unencrypted connection", "Közvetlen, és nem titkosított kapcsolat"), ("Relayed and unencrypted connection", "Továbbított, és nem titkosított kapcsolat"), ("Enter Remote ID", "Távoli számítógép azonosítója"), - ("Enter your password", "Írja be a jelszavát"), - ("Logging in...", "A belépés folyamatban..."), + ("Enter your password", "Adja meg a jelszavát"), + ("Logging in...", "Belépés folyamatban…"), ("Enable RDP session sharing", "RDP-munkamenet-megosztás engedélyezése"), ("Auto Login", "Automatikus bejelentkezés"), ("Enable direct IP access", "Közvetlen IP-elérés engedélyezése"), @@ -196,32 +196,32 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Create desktop shortcut", "Asztali parancsikon létrehozása"), ("Change Path", "Elérési út módosítása"), ("Create Folder", "Mappa létrehozás"), - ("Please enter the folder name", "Kérjük, adja meg a mappa nevét"), + ("Please enter the folder name", "Adja meg a mappa nevét"), ("Fix it", "Javítás"), ("Warning", "Figyelmeztetés"), ("Login screen using Wayland is not supported", "Bejelentkezéskori Wayland használata nem támogatott"), ("Reboot required", "Újraindítás szükséges"), - ("Unsupported display server", "Nem támogatott megjelenítő szerver"), + ("Unsupported display server", "Nem támogatott megjelenítő kiszolgáló"), ("x11 expected", "x11-re számítottt"), ("Port", "Port"), ("Settings", "Beállítások"), ("Username", "Felhasználónév"), ("Invalid port", "Érvénytelen port"), - ("Closed manually by the peer", "A kapcsolatot a másik fél manuálisan bezárta"), - ("Enable remote configuration modification", "Távoli konfiguráció módosítás engedélyezése"), - ("Run without install", "Futtatás feltelepítés nélkül"), - ("Connect via relay", "Csatlakozás közvetítőn keresztül"), - ("Always connect via relay", "Mindig közvetítőn keresztüli csatlakozás"), - ("whitelist_tip", "Csak az engedélyezési listán szereplő címek csatlakozhatnak"), + ("Closed manually by the peer", "A kapcsolatot a másik fél kézileg bezárta"), + ("Enable remote configuration modification", "Távoli konfiguráció-módosítás engedélyezése"), + ("Run without install", "Futtatás telepítés nélkül"), + ("Connect via relay", "Kapcsolódás továbbító-kiszolgálón keresztül"), + ("Always connect via relay", "Kapcsolódás mindig továbbító-kiszolgálón keresztül"), + ("whitelist_tip", "Csak az engedélyezési listán szereplő címek kapcsolódhatnak"), ("Login", "Belépés"), ("Verify", "Ellenőrzés"), - ("Remember me", "Emlékezz rám"), - ("Trust this device", "Bízzon ebben az eszközben"), + ("Remember me", "Emlékezzen rám"), + ("Trust this device", "Megbízom ebben az eszközben"), ("Verification code", "Ellenőrző kód"), - ("verification_tip", "A regisztrált e-mail címre egy ellenőrző kódot küldtek. Adja meg az ellenőrző kódot az újbóli bejelentkezéshez."), + ("verification_tip", "A regisztrált e-mail-címre egy ellenőrző kód lesz elküldve. Adja meg az ellenőrző kódot az újbóli bejelentkezéshez."), ("Logout", "Kilépés"), ("Tags", "Címkék"), - ("Search ID", "Azonosító keresése..."), + ("Search ID", "Azonosító keresése…"), ("whitelist_sep", "A címeket veszővel, pontosvesszővel, szóközzel, vagy új sorral válassza el"), ("Add ID", "Azonosító hozzáadása"), ("Add Tag", "Címke hozzáadása"), @@ -230,9 +230,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Username missed", "Üres felhasználónév"), ("Password missed", "Üres jelszó"), ("Wrong credentials", "Hibás felhasználónév vagy jelszó"), - ("The verification code is incorrect or has expired", ""), + ("The verification code is incorrect or has expired", "A hitelesítőkód érvénytelen vagy lejárt"), ("Edit Tag", "Címke szerkesztése"), - ("Forget Password", "A jelszó megjegyzésének törlése"), + ("Forget Password", "A jelszó megjegyzésének megszüntetése"), ("Favorites", "Kedvencek"), ("Add to Favorites", "Hozzáadás a kedvencekhez"), ("Remove from Favorites", "Eltávolítás a kedvencekből"), @@ -244,9 +244,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("install_daemon_tip", "Az automatikus indításhoz szükséges a szolgáltatás telepítése"), ("Remote ID", "Távoli azonosító"), ("Paste", "Beillesztés"), - ("Paste here?", "Beilleszti ide?"), - ("Are you sure to close the connection?", "Biztos, hogy bezárja a kapcsolatot?"), - ("Download new version", "Új verzó letöltése"), + ("Paste here?", "Beillesztés ide?"), + ("Are you sure to close the connection?", "Biztosan bezárja a kapcsolatot?"), + ("Download new version", "Új verzió letöltése"), ("Touch mode", "Érintési mód bekapcsolása"), ("Mouse mode", "Egérhasználati mód bekapcsolása"), ("One-Finger Tap", "Egyujjas érintés"), @@ -259,56 +259,56 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Mouse Drag", "Mozgatás egérrel"), ("Three-Finger vertically", "Három ujj függőlegesen"), ("Mouse Wheel", "Egérgörgő"), - ("Two-Finger Move", "Kátujjas mozgatás"), + ("Two-Finger Move", "Kétujjas mozgatás"), ("Canvas Move", "Nézet mozgatása"), ("Pinch to Zoom", "Kétujjas nagyítás"), ("Canvas Zoom", "Nézet nagyítása"), ("Reset canvas", "Nézet visszaállítása"), ("No permission of file transfer", "Nincs engedély a fájlátvitelre"), - ("Note", "Megyjegyzés"), + ("Note", "Megjegyzés"), ("Connection", "Kapcsolat"), ("Share Screen", "Képernyőmegosztás"), ("Chat", "Csevegés"), ("Total", "Összes"), ("items", "elemek"), - ("Selected", "Kijelölt"), + ("Selected", "Kijelölve"), ("Screen Capture", "Képernyőrögzítés"), ("Input Control", "Távoli vezérlés"), ("Audio Capture", "Hangrögzítés"), ("File Connection", "Fájlátvitel"), ("Screen Connection", "Képátvitel"), - ("Do you accept?", "Elfogadja?"), + ("Do you accept?", "Elfogadás?"), ("Open System Setting", "Rendszerbeállítások megnyitása"), - ("How to get Android input permission?", "Hogyan állíthatok be Android beviteli engedélyt?"), - ("android_input_permission_tip1", "A távoli vezérléshez kérjük engedélyezze a \"Kisegítő lehetőség\" lehetőséget."), + ("How to get Android input permission?", "Hogyan állítható be az Androidos beviteli engedély?"), + ("android_input_permission_tip1", "A távoli vezérléshez engedélyezze a „Kisegítő lehetőségek” lehetőséget."), ("android_input_permission_tip2", "A következő rendszerbeállítások oldalon a letöltött alkalmazások menüponton belül, kapcsolja be a [RustDesk Input] szolgáltatást."), - ("android_new_connection_tip", "Új kérés érkezett mely vezérelni szeretné az eszközét"), - ("android_service_will_start_tip", "A \"Képernyőrögzítés\" bekapcsolásával automatikus elindul a szolgáltatás, lehetővé téve, hogy más eszközök csatlakozási kérelmet küldhessenek"), + ("android_new_connection_tip", "Új kérés érkezett, mely vezérelni szeretné az eszközét"), + ("android_service_will_start_tip", "A „Képernyőrögzítés” bekapcsolásával automatikus elindul a szolgáltatás, lehetővé téve, hogy más eszközök kapcsolódási kérelmet küldhessenek"), ("android_stop_service_tip", "A szolgáltatás leállítása automatikusan szétkapcsol minden létező kapcsolatot."), ("android_version_audio_tip", "A jelenlegi Android verzió nem támogatja a hangrögzítést, frissítsen legalább Android 10-re, vagy egy újabb verzióra."), - ("android_start_service_tip", "A képernyőmegosztó szolgáltatás elindításához koppintson a \" Közvetítő szolgáltatás indítása\" gombra, vagy aktiválja a \"Képernyőfelvétel\" engedélyt."), - ("android_permission_may_not_change_tip", "A meglévő kapcsolatok engedélyei csak új csatlakozás után módosulnak."), + ("android_start_service_tip", "A képernyőmegosztó szolgáltatás elindításához koppintson a „továbbító-kiszolgáló-szolgáltatás indítása” gombra, vagy aktiválja a „Képernyőfelvétel” engedélyt."), + ("android_permission_may_not_change_tip", "A meglévő kapcsolatok engedélyei csak új kapcsolódás után módosulnak."), ("Account", "Fiók"), ("Overwrite", "Felülírás"), ("This file exists, skip or overwrite this file?", "Ez a fájl már létezik, kihagyja vagy felülírja ezt a fájlt?"), ("Quit", "Kilépés"), - ("Help", "Segítség"), + ("Help", "Súgó"), ("Failed", "Sikertelen"), ("Succeeded", "Sikeres"), ("Someone turns on privacy mode, exit", "Valaki bekacsolta az inkognitó módot, lépjen ki"), ("Unsupported", "Nem támogatott"), - ("Peer denied", "Elutasítva a távoli fél álltal"), - ("Please install plugins", "Kérem telepítse a bővítményeket"), + ("Peer denied", "Elutasítva a távoli fél által"), + ("Please install plugins", "Telepítse a bővítményeket"), ("Peer exit", "A távoli fél kilépett"), ("Failed to turn off", "Nem sikerült kikapcsolni"), ("Turned off", "Kikapcsolva"), ("Language", "Nyelv"), ("Keep RustDesk background service", "RustDesk futtatása a háttérben"), - ("Ignore Battery Optimizations", "Akkumulátorkímélő figyelmen kívűl hagyása"), - ("android_open_battery_optimizations_tip", "Ha le szeretné tiltani ezt a funkciót, lépjen a RustDesk alkalmazás beállítási oldalára, keresse meg az [Akkumulátorkímélő] lehetőséget és válassza a nincs korlátozás lehetőséget."), + ("Ignore Battery Optimizations", "Akkumulátorkímélő figyelmen kívül hagyása"), + ("android_open_battery_optimizations_tip", "Ha le szeretné tiltani ezt a funkciót, lépjen a RustDesk alkalmazás beállításaiba, keresse meg az [Akkumulátorkímélő] lehetőséget és válassza a nincs korlátozás lehetőséget."), ("Start on boot", "Indítás bekapcsoláskor"), ("Start the screen sharing service on boot, requires special permissions", "Indítsa el a képernyőmegosztó szolgáltatást rendszerindításkor, speciális engedélyeket igényel"), - ("Connection not allowed", "A csatlakozás nem engedélyezett"), + ("Connection not allowed", "A kapcsolódás nem engedélyezett"), ("Legacy mode", "Kompatibilitási mód"), ("Map mode", "Hozzárendelési mód"), ("Translate mode", "Fordító mód"), @@ -317,9 +317,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Set permanent password", "Állandó jelszó beállítása"), ("Enable remote restart", "Távoli újraindítás engedélyezése"), ("Restart remote device", "Távoli eszköz újraindítása"), - ("Are you sure you want to restart", "Biztos szeretné újraindítani?"), - ("Restarting remote device", "Távoli eszköz újraindítása..."), - ("remote_restarting_tip", "A távoli eszköz újraindul, zárja be ezt az üzenetet, csatlakozzon újra, állandó jelszavával"), + ("Are you sure you want to restart", "Biztosan újra szeretné indítani?"), + ("Restarting remote device", "Távoli eszköz újraindítása…"), + ("remote_restarting_tip", "A távoli eszköz újraindul, zárja be ezt az üzenetet, kapcsolódjon újra az állandó jelszavával"), ("Copied", "Másolva"), ("Exit Fullscreen", "Kilépés teljes képernyős módból"), ("Fullscreen", "Teljes képernyő"), @@ -331,11 +331,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Image Quality", "Képminőség"), ("Scroll Style", "Görgetési stílus"), ("Show Toolbar", "Eszköztár megjelenítése"), - ("Hide Toolbar", "Eszköztár eljertése"), - ("Direct Connection", "Közvetlen kapcsolat"), - ("Relay Connection", "Közvetett csatlakozás"), - ("Secure Connection", "Biztonságos kapcsolat"), - ("Insecure Connection", "Nem biztonságos kapcsolat"), + ("Hide Toolbar", "Eszköztár elrejtése"), + ("Direct Connection", "Kapcsolódás közvetlenül"), + ("Relay Connection", "Kapcsolódás továbbító-kiszolgálón keresztül"), + ("Secure Connection", "Biztonságos kapcsolódás"), + ("Insecure Connection", "Nem biztonságos kapcsolódás"), ("Scale original", "Eredeti méretarány"), ("Scale adaptive", "Adaptív méretarány"), ("General", "Általános"), @@ -345,13 +345,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Light Theme", "Világos téma"), ("Dark", "Sötét"), ("Light", "Világos"), - ("Follow System", "Rendszer téma követése"), + ("Follow System", "Rendszer beállításainak követése"), ("Enable hardware codec", "Hardveres kodek engedélyezése"), ("Unlock Security Settings", "Biztonsági beállítások feloldása"), ("Enable audio", "Hang engedélyezése"), ("Unlock Network Settings", "Hálózati beállítások feloldása"), - ("Server", "Szerver"), - ("Direct IP Access", "Közvetlen IP hozzáférés"), + ("Server", "Kiszolgáló"), + ("Direct IP Access", "Közvetlen IP-hozzáférés"), ("Proxy", "Proxy"), ("Apply", "Alkalmaz"), ("Disconnect all devices?", "Leválasztja az összes eszközt?"), @@ -369,22 +369,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Start session recording", "Munkamenet rögzítés indítása"), ("Stop session recording", "Munkamenet rögzítés leállítása"), ("Enable recording session", "Munkamenet rögzítés engedélyezése"), - ("Enable LAN discovery", "Felfedezés enegedélyezése"), + ("Enable LAN discovery", "Felfedezés engedélyezése"), ("Deny LAN discovery", "Felfedezés tiltása"), ("Write a message", "Üzenet írása"), ("Prompt", "Kérés"), - ("Please wait for confirmation of UAC...", "Kérjük, várjon az UAC megerősítésére..."), + ("Please wait for confirmation of UAC...", "Várjon az UAC megerősítésére…"), ("elevated_foreground_window_tip", "A távvezérelt számítógép jelenleg nyitott ablakához magasabb szintű jogok szükségesek. Ezért jelenleg nem lehetséges az egér és a billentyűzet használata. Kérje meg azt a felhasználót, akinek a számítógépét távolról vezérli, hogy minimalizálja az ablakot, vagy növelje a jogokat. A jövőbeni probléma elkerülése érdekében ajánlott a szoftvert a távvezérelt számítógépre telepíteni."), - ("Disconnected", "Szétkapcsolva"), + ("Disconnected", "Kapcsolat bontva"), ("Other", "Egyéb"), - ("Confirm before closing multiple tabs", "Biztos, hogy bezárja az összes lapot?"), + ("Confirm before closing multiple tabs", "Biztosan bezárja az összes lapot?"), ("Keyboard Settings", "Billentyűzet beállítások"), ("Full Access", "Teljes hozzáférés"), ("Screen Share", "Képernyőmegosztás"), - ("Wayland requires Ubuntu 21.04 or higher version.", "A Waylandhoz Ubuntu 21.04 vagy újabb verzió szükséges."), - ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "A Wayland a Linux disztró magasabb verzióját igényli. Próbálja ki az X11 desktopot, vagy változtassa meg az operációs rendszert."), + ("Wayland requires Ubuntu 21.04 or higher version.", "A Waylandhez Ubuntu 21.04 vagy újabb verzió szükséges."), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "A Wayland a Linux disztribúció magasabb verzióját igényli. Próbálja ki az X11 desktopot, vagy változtassa meg az operációs rendszert."), ("JumpLink", "Hiperhivatkozás"), - ("Please Select the screen to be shared(Operate on the peer side).", "Kérjük, válassza ki a megosztani kívánt képernyőt."), + ("Please Select the screen to be shared(Operate on the peer side).", "Válassza ki a megosztani kívánt képernyőt."), ("Show RustDesk", "A RustDesk megjelenítése"), ("This PC", "Ez a számítógép"), ("or", "vagy"), @@ -394,35 +394,35 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Accept sessions via password", "Munkamenetek elfogadása jelszóval"), ("Accept sessions via click", "Munkamenetek elfogadása kattintással"), ("Accept sessions via both", "Munkamenetek fogadása mindkettőn keresztül"), - ("Please wait for the remote side to accept your session request...", "Kérjük, várjon, amíg a távoli oldal elfogadja a munkamenet-kérelmét..."), + ("Please wait for the remote side to accept your session request...", "Várjon, amíg a távoli oldal elfogadja a munkamenet-kérelmét…"), ("One-time Password", "Egyszer használatos jelszó"), ("Use one-time password", "Használjon ideiglenes jelszót"), ("One-time password length", "Egyszer használatos jelszó hossza"), ("Request access to your device", "Hozzáférés kérése az eszközéhez"), ("Hide connection management window", "Kapcsolatkezelő ablak elrejtése"), ("hide_cm_tip", "Ez csak akkor lehetséges, ha a hozzáférés állandó jelszóval történik."), - ("wayland_experiment_tip", "A Wayland-támogatás csak kísérleti jellegű. Kérjük, használja az X11-et, ha felügyelet nélküli hozzáférésre van szüksége."), + ("wayland_experiment_tip", "A Wayland-támogatás csak kísérleti jellegű. Használja az X11-et, ha felügyelet nélküli hozzáférésre van szüksége."), ("Right click to select tabs", "Jobb klikk a lapok kiválasztásához"), ("Skipped", "Kihagyott"), ("Add to address book", "Hozzáadás a címjegyzékhez"), ("Group", "Csoport"), ("Search", "Keresés"), - ("Closed manually by web console", "Kézzel zárva a webkonzolon keresztül"), + ("Closed manually by web console", "Kézzel bezárva a webkonzolon keresztül"), ("Local keyboard type", "Helyi billentyűzet típusa"), ("Select local keyboard type", "Helyi billentyűzet típusának kiválasztása"), ("software_render_tip", "Ha Nvidia grafikus kártyát használ Linux alatt, és a távoli ablak a kapcsolat létrehozása után azonnal bezáródik, akkor a Nouveau nyílt forráskódú illesztőprogramra való váltás és a szoftveres renderelés használata segíthet. A szoftvert újra kell indítani."), ("Always use software rendering", "Mindig szoftveres renderelést használjon"), - ("config_input", "Ahhoz, hogy a távoli asztalt a billentyűzettel vezérelhesse, a RustDesknek meg kell adnia a \" Bemenet figyelése\" jogosultságot."), - ("config_microphone", "Ahhoz, hogy távolról beszélhessen, meg kell adnia a RustDesknek a \"Hangfelvétel\" jogosultságot."), + ("config_input", "Ahhoz, hogy a távoli asztalt a billentyűzettel vezérelhesse, a RustDesknek meg kell adnia a „Bemenet figyelése” jogosultságot."), + ("config_microphone", "Ahhoz, hogy távolról beszélhessen, meg kell adnia a RustDesknek a „Hangfelvétel” jogosultságot."), ("request_elevation_tip", "Akkor is kérhet megnövelt jogokat, ha valaki a partneroldalon van."), ("Wait", "Várjon"), - ("Elevation Error", "Emeltszintű hozzáférési hiba"), + ("Elevation Error", "Emelt szintű hozzáférési hiba"), ("Ask the remote user for authentication", "Hitelesítés kérése a távoli felhasználótól"), - ("Choose this if the remote account is administrator", "Válassza ezt, ha a távoli fiók rendszergazda"), + ("Choose this if the remote account is administrator", "Akkor válassza ezt, ha a távoli fiók rendszergazda"), ("Transmit the username and password of administrator", "Küldje el a rendszergazda felhasználónevét és jelszavát"), - ("still_click_uac_tip", "A távoli felhasználónak továbbra is a \"Igen\" gombra kell kattintania a RustDesk UAC ablakában. Kattintson!"), + ("still_click_uac_tip", "A távoli felhasználónak továbbra is az „Igen” gombra kell kattintania a RustDesk UAC ablakában. Kattintson!"), ("Request Elevation", "Emelt szintű jogok igénylése"), - ("wait_accept_uac_tip", "Kérjük, várjon, amíg a távoli felhasználó elfogadja az UAC párbeszédet."), + ("wait_accept_uac_tip", "Várjon, amíg a távoli felhasználó elfogadja az UAC párbeszédet."), ("Elevate successfully", "Emelt szintű jogok megadva"), ("uppercase", "NAGYBETŰS"), ("lowercase", "kisbetűs"), @@ -433,12 +433,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Medium", "Közepes"), ("Strong", "Erős"), ("Switch Sides", "Oldalváltás"), - ("Please confirm if you want to share your desktop?", "Kérjük, erősítse meg, hogy meg akarja-e osztani az asztalát?"), + ("Please confirm if you want to share your desktop?", "Erősítse meg, hogy meg akarja-e osztani az asztalát?"), ("Display", "Képernyő"), ("Default View Style", "Alapértelmezett megjelenítés"), ("Default Scroll Style", "Alapértelmezett görgetés"), ("Default Image Quality", "Alapértelmezett képminőség"), - ("Default Codec", "Alapértelmezett kódek"), + ("Default Codec", "Alapértelmezett kodek"), ("Bitrate", "Bitsebesség"), ("FPS", "FPS"), ("Auto", "Automatikus"), @@ -446,9 +446,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", "Hanghívás"), ("Text chat", "Szöveges csevegés"), ("Stop voice call", "Hanghívás leállítása"), - ("relay_hint_tip", "Ha a közvetlen kapcsolat nem lehetséges, megpróbálhat kapcsolatot létesíteni egy relé-kiszolgálón keresztül.\nHa az első próbálkozáskor relé-kapcsolatot szeretne létrehozni, használhatja a \"/r\" utótagot. az azonosítóhoz vagy a \"Mindig relé-kiszolgálón keresztül csatlakozom\" opcióhoz a legutóbbi munkamenetek listájában, ha van ilyen."), - ("Reconnect", "Újracsatlakoztatás"), - ("Codec", "Kódek"), + ("relay_hint_tip", "Ha a közvetlen kapcsolat nem lehetséges, megpróbálhat kapcsolatot létesíteni egy továbbító-kiszolgálón keresztül.\nHa az első próbálkozáskor továbbító-kiszolgálón keresztüli kapcsolatot szeretne létrehozni, használhatja az „/r” utótagot. az azonosítóhoz vagy a „Mindig továbbító-kiszolgálón keresztül kapcsolódom” opcióhoz a legutóbbi munkamenetek listájában, ha van ilyen."), + ("Reconnect", "Újrakapcsolódás"), + ("Codec", "Kodek"), ("Resolution", "Felbontás"), ("No transfers in progress", "Nincs folyamatban átvitel"), ("Set one-time password length", "Állítsa be az egyszeri jelszó hosszát"), @@ -459,14 +459,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Minimize", "Minimalizálás"), ("Maximize", "Maximalizálás"), ("Your Device", "Az Ön eszköze"), - ("empty_recent_tip", "Hoppá, nincsenek aktuális munkamenetek!\nIdeje ütemezni egy újat."), - ("empty_favorite_tip", "Még nincs kedvenc távoli állomás?\nHagyd, hogy találjunk valakit, akivel kapcsolatba tudunk lépni, és add hozzá a kedvenceidhez!"), - ("empty_lan_tip", "Ó, nem, úgy tűnik, még nem fedeztünk fel egy távoli helyszínt."), - ("empty_address_book_tip", "Ó, kedvesem, úgy tűnik, hogy jelenleg nincsenek távoli állomások a címjegyzékében."), - ("eg: admin", "pl: admin"), + ("empty_recent_tip", "Nincsenek aktuális munkamenetek!\nIdeje ütemezni egy újat."), + ("empty_favorite_tip", "Még nincs kedvenc távoli állomása?\nHagyja, hogy találjunk valakit, akivel kapcsolatba tud lépni, és add hozzá a kedvenceidhez!"), + ("empty_lan_tip", "Úgy tűnik, még nem adott hozzá egyetlen távoli helyszínt sem."), + ("empty_address_book_tip", "Úgy tűnik, hogy jelenleg nincsenek távoli állomások a címjegyzékében."), + ("eg: admin", "pl: adminisztrátor"), ("Empty Username", "Üres felhasználónév"), ("Empty Password", "Üres jelszó"), - ("Me", "Én"), + ("Me", "Ön"), ("identical_file_tip", "Ez a fájl megegyezik a távoli állomás fájljával."), ("show_monitors_tip", "Képernyők megjelenítése az eszköztáron"), ("View Mode", "Nézet mód"), @@ -478,11 +478,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("another_user_login_title_tip", "Egy másik felhasználó már bejelentkezett."), ("another_user_login_text_tip", "Különálló"), ("xorg_not_found_title_tip", "Xorg nem található."), - ("xorg_not_found_text_tip", "Kérjük, telepítse az Xorg-ot."), + ("xorg_not_found_text_tip", "Telepítse az Xorgot."), ("no_desktop_title_tip", "Nem áll rendelkezésre asztali környezet."), - ("no_desktop_text_tip", "Kérjük, telepítse a GNOME asztalt."), - ("No need to elevate", ""), - ("System Sound", "A jogok növelése nem szükséges"), + ("no_desktop_text_tip", "Telepítse a GNOME asztali környezetet."), + ("No need to elevate", "Nem szükséges megemelni"), + ("System Sound", "Rendszer hangok"), ("Default", "Alapértelmezett"), ("New RDP", "Új RDP"), ("Fingerprint", "Ujjlenyomat"), @@ -498,16 +498,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Options", "Beállítások"), ("resolution_original_tip", "Eredeti felbontás"), ("resolution_fit_local_tip", "Helyi felbontás beállítása"), - ("resolution_custom_tip", "Testreszabható felbontás"), + ("resolution_custom_tip", "Testre szabható felbontás"), ("Collapse toolbar", "Eszköztár összecsukása"), - ("Accept and Elevate", "Elfogadás és magasabb szintű jogrosultságra emelés"), + ("Accept and Elevate", "Elfogadás és magasabb szintű jogosultságra emelés"), ("accept_and_elevate_btn_tooltip", "Fogadja el a kapcsolatot, és növelje az UAC-engedélyeket."), ("clipboard_wait_response_timeout_tip", "Időtúllépés, amíg a másolat válaszára vár."), ("Incoming connection", "Bejövő kapcsolat"), ("Outgoing connection", "Kimenő kapcsolat"), ("Exit", "Kilépés"), ("Open", "Megnyitás"), - ("logout_tip", "Biztosan le szeretne iratkozni?"), + ("logout_tip", "Biztosan ki szeretne lépni?"), ("Service", "Szolgáltatás"), ("Start", "Indítás"), ("Stop", "Leállítás"), @@ -524,9 +524,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Grid View", "Mozaik nézet"), ("List View", "Lista nézet"), ("Select", "Kiválasztás"), - ("Toggle Tags", "Címke kapcsoló"), + ("Toggle Tags", "Címkekapcsoló"), ("pull_ab_failed_tip", "A címjegyzék frissítése nem sikerült"), - ("push_ab_failed_tip", "A címjegyzék szinkronizálása a szerverrel nem sikerült"), + ("push_ab_failed_tip", "A címjegyzék szinkronizálása a kiszolgálóval nem sikerült"), ("synced_peer_readded_tip", "A legutóbbi munkamenetekben jelen lévő eszközök ismét felkerülnek a címjegyzékbe."), ("Change Color", "Szín módosítása"), ("Primary Color", "Elsődleges szín"), @@ -538,14 +538,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("scam_title", "Lehet, hogy átverték!"), ("scam_text1", "Ha olyan valakivel beszél telefonon, akit NEM ISMER, akiben NEM BÍZIK MEG, és aki arra kéri, hogy használja a RustDesket és indítsa el a szolgáltatást, ne folytassa, és azonnal tegye le a telefont."), ("scam_text2", "Valószínűleg egy csaló próbálja ellopni a pénzét vagy más személyes adatait."), - ("Don't show again", "Ne mutasd újra"), - ("I Agree", "Elfogadom"), - ("Decline", "Elutasítom"), + ("Don't show again", "Ne jelenítse meg újra"), + ("I Agree", "Elfogadás"), + ("Decline", "Elutasítás"), ("Timeout in minutes", "Időtúllépés percekben"), ("auto_disconnect_option_tip", "A bejövő munkamenetek automatikus bezárása, ha a felhasználó inaktív"), ("Connection failed due to inactivity", "A kapcsolat inaktivitás miatt megszakadt"), ("Check for software update on startup", "Szoftverfrissítés keresése indításkor"), - ("upgrade_rustdesk_server_pro_to_{}_tip", "Kérjük, frissítse a RustDesk Server Pro-t a(z) {} vagy újabb verzióra!"), + ("upgrade_rustdesk_server_pro_to_{}_tip", "Frissítse a RustDesk Server Prot a(z) {} vagy újabb verzióra!"), ("pull_group_failed_tip", "A csoport frissítése nem sikerült"), ("Filter by intersection", "Szűrés metszéspontok szerint"), ("Remove wallpaper during incoming sessions", "Távolítsa el a háttérképet a bejövő munkamenetek során"), @@ -564,7 +564,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Plug out all", "Kapcsolja ki az összeset"), ("True color (4:4:4)", "Valódi szín (4:4:4)"), ("Enable blocking user input", "Engedélyezze a felhasználói bevitel blokkolását"), - ("id_input_tip", "Megadhat egy azonosítót, egy közvetlen IP címet vagy egy tartományt egy porttal (:).\nHa egy másik szerveren lévő eszközhöz szeretne hozzáférni, adja meg a szerver címét (@?key=), például\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nHa egy nyilvános szerveren lévő eszközhöz szeretne hozzáférni, adja meg a \"@public\" lehetőséget. in. A kulcsra nincs szükség nyilvános szerverek esetén.\n\nHa az első kapcsolathoz relé-kapcsolatot akar kényszeríteni, adjon hozzá \"/r\" az azonosító végén, például \"9123456234/r\"."), + ("id_input_tip", "Megadhat egy azonosítót, egy közvetlen IP-címet vagy egy tartományt egy porttal (:).\nHa egy másik kiszolgálón lévő eszközhöz szeretne hozzáférni, adja meg a kiszolgáló címét (@?key=), például\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nHa egy nyilvános kiszolgálón lévő eszközhöz szeretne hozzáférni, adja meg a „@public” lehetőséget. in. A kulcsra nincs szükség nyilvános kiszolgálók esetén.\n\nHa az első kapcsolathoz továbbító-kiszolgálón keresztüli kapcsolatot akar kényszeríteni, adja hozzá az „/r” az azonosítót a végén, például „9123456234/r”."), ("privacy_mode_impl_mag_tip", "1. mód"), ("privacy_mode_impl_virtual_display_tip", "2. mód"), ("Enter privacy mode", "Lépjen be az adatvédelmi módba"), @@ -575,20 +575,20 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Swap control-command key", "Vezérlő- és parancsgombok cseréje"), ("swap-left-right-mouse", "Bal és jobb egérgomb felcserélése"), ("2FA code", "2FA kód"), - ("More", "További"), + ("More", "Továbbiak"), ("enable-2fa-title", "Kétfaktoros hitelesítés aktiválása"), - ("enable-2fa-desc", "Kérjük, most állítsa be a hitelesítőt. Használhat egy hitelesítési alkalmazást, például az Authy, a Microsoft vagy a Google Authenticator alkalmazást a telefonján vagy az asztali számítógépén.\n\nScannelje be a QR-kódot az alkalmazással, és adja meg az alkalmazás által megjelenített kódot a kétfaktoros hitelesítés aktiválásához."), + ("enable-2fa-desc", "Állítsa be a hitelesítőt. Használhat egy hitelesítő alkalmazást, például az Aegis, Authy, a Microsoft- vagy a Google Authenticator alkalmazást a telefonján vagy az asztali számítógépén.\n\nOlvassa be a QR-kódot az alkalmazással, és adja meg az alkalmazás által megjelenített kódot a kétfaktoros hitelesítés aktiválásához."), ("wrong-2fa-code", "A kód nem ellenőrizhető. Ellenőrizze, hogy a kód és a helyi idő beállításai helyesek-e."), ("enter-2fa-title", "Kétfaktoros hitelesítés"), - ("Email verification code must be 6 characters.", "Az e-mail ellenőrző kódnak 6 karakterből kell állnia."), + ("Email verification code must be 6 characters.", "Az e-mailben kapott ellenőrző-kódnak 6 karakterből kell állnia."), ("2FA code must be 6 digits.", "A 2FA-kódnak 6 számjegyűnek kell lennie."), ("Multiple Windows sessions found", "Több Windows munkamenet található"), - ("Please select the session you want to connect to", "Kérjük, válassza ki a munkamenetet, amelyhez csatlakozni szeretne"), + ("Please select the session you want to connect to", "Válassza ki a munkamenetet, amelyhez kapcsolódni szeretne"), ("powered_by_me", "Üzemeltető: RustDesk"), - ("outgoing_only_desk_tip", "Ez a RustDesk testreszabott kimenete.\nMás eszközökhöz csatlakozhat, de más eszközök nem csatlakozhatnak az Ön eszközéhez."), - ("preset_password_warning", "Ez egy testreszabott kimenet a RustDeskből egy előre beállított jelszóval. Bárki, aki ismeri ezt a jelszót, teljes irányítást szerezhet a készülék felett. Ha nem kívánja ezt megtenni, kérjük, azonnal távolítsa el ezt a szoftvert."), + ("outgoing_only_desk_tip", "Ez a RustDesk testre szabott kimenete.\nMás eszközökhöz kapcsolódhat, de más eszközök nem kapcsolódhatnak az Ön eszközéhez."), + ("preset_password_warning", "Ez egy testre szabott kimenet a RustDeskből egy előre beállított jelszóval. Bárki, aki ismeri ezt a jelszót, teljes irányítást szerezhet a készülék felett. Ha nem kívánja ezt megtenni, azonnal távolítsa el ezt a szoftvert."), ("Security Alert", "Biztonsági riasztás"), - ("My address book", "Címjegyzékem"), + ("My address book", "Saját címjegyzék"), ("Personal", "Személyes"), ("Owner", "Tulajdonos"), ("Set shared password", "Megosztott jelszó beállítása"), @@ -599,7 +599,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("share_warning_tip", "A fenti mezők megosztottak és mások számára is láthatóak."), ("Everyone", "Mindenki"), ("ab_web_console_tip", "További információk a webes konzolról"), - ("allow-only-conn-window-open-tip", "Csak akkor engedélyezze a csatlakozást, ha a RustDesk ablak nyitva van."), + ("allow-only-conn-window-open-tip", "Csak akkor engedélyezze a kapcsolódást, ha a RustDesk ablak nyitva van."), ("no_need_privacy_mode_no_physical_displays_tip", "Nincsenek fizikai képernyők; Nincs szükség az adatvédelmi üzemmód használatára."), ("Follow remote cursor", "Kövesse a távoli kurzort"), ("Follow remote window focus", "Kövesse a távoli ablak fókuszt"), @@ -627,12 +627,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Power", "Teljesítmény"), ("Telegram bot", "Telegram bot"), ("enable-bot-tip", "Ha aktiválja ezt a funkciót, akkor a 2FA-kódot a botjától kaphatja meg. Kapcsolati értesítésként is használható."), - ("enable-bot-desc", "1. Nyisson csevegést @BotFather.\n2. Küldje el a \"/newbot\" parancsot. Miután ezt a lépést elvégezte, kap egy tokent.\n3. Indítson csevegést az újonnan létrehozott botjával. Küldjön egy olyan üzenetet, amely egy perjelrel kezdődik (\"/\"), pl. B. \"/hello\" az aktiváláshoz.\n"), - ("cancel-2fa-confirm-tip", "Biztos, hogy le akarja mondani a 2FA-t?"), - ("cancel-bot-confirm-tip", "Biztos, hogy le akarod mondani a Telegram botot?"), - ("About RustDesk", "A RustDeskről"), - ("Send clipboard keystrokes", "Vágólap billentyűleütések küldése"), - ("network_error_tip", "Kérjük, ellenőrizze a hálózati kapcsolatot, majd próbálja meg újra."), + ("enable-bot-desc", "1. Nyisson csevegést @BotFather.\n2. Küldje el a „/newbot” parancsot. Miután ezt a lépést elvégezte, kap egy tokent.\n3. Indítson csevegést az újonnan létrehozott botjával. Küldjön egy olyan üzenetet, amely egy perjelrel kezdődik („/”), pl. B. „/hello” az aktiváláshoz.\n"), + ("cancel-2fa-confirm-tip", "Biztosan le akarja mondani a 2FA-t?"), + ("cancel-bot-confirm-tip", "Biztosan le akarja mondani a Telegram botot?"), + ("About RustDesk", "RustDesk névjegye"), + ("Send clipboard keystrokes", "Billentyűleütések küldése a vágólapra"), + ("network_error_tip", "Ellenőrizze a hálózati kapcsolatot, majd próbálja meg újra."), ("Unlock with PIN", "Feloldás PIN-kóddal"), ("Requires at least {} characters", "Legalább {} karakter szükséges"), ("Wrong PIN", "Hibás PIN"), @@ -648,13 +648,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("one-way-file-transfer-tip", "Az egyirányú fájlátvitel engedélyezve van a vezérelt oldalon."), ("Authentication Required", "Hitelesítés szükséges"), ("Authenticate", "Hitelesítés"), - ("web_id_input_tip", "Azonos szerveren lévő azonosítót adhat meg, a közvetlen IP elérés nem támogatott a webkliensben.\nHa egy másik szerveren lévő eszközhöz szeretne hozzáférni, kérjük, adja meg a szerver címét (@?key=), például\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nHa egy nyilvános szerveren lévő eszközhöz szeretne hozzáférni, kérjük, adja meg a \"@public\" betűt. in. A kulcsra nincs szükség a nyilvános szerverek esetében."), + ("web_id_input_tip", "Azonos kiszolgálón lévő azonosítót adhat meg, a közvetlen IP elérés nem támogatott a webkliensben.\nHa egy másik kiszolgálón lévő eszközhöz szeretne hozzáférni, adja meg a kiszolgáló címét (@?key=), például\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nHa egy nyilvános kiszolgálón lévő eszközhöz szeretne hozzáférni, adja meg a „@public” betűt. in. A kulcsra nincs szükség a nyilvános kiszolgálók esetében."), ("Download", "Letöltés"), ("Upload folder", "Mappa feltöltése"), ("Upload files", "Fájlok feltöltése"), ("Clipboard is synchronized", "A vágólap szinkronizálva van"), - ("Update client clipboard", ""), - ("Untagged", ""), - ("new-version-of-{}-tip", ""), + ("Update client clipboard", "A kliens vágólapjának frissítése"), + ("Untagged", "Címkézetlen"), + ("new-version-of-{}-tip", "A(z) {} új verziója"), ].iter().cloned().collect(); } From 771cc565ab343c9bacad0f2951bc14d94cda5a88 Mon Sep 17 00:00:00 2001 From: Dmitry Beskov <43372966+besdar@users.noreply.github.com> Date: Mon, 16 Dec 2024 12:04:53 +0400 Subject: [PATCH 477/541] Flathub badge in the README (#10288) * new flathub badge in readme * replacing badge with svg --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index c193967d0b56..b5980bd7a20a 100644 --- a/README.md +++ b/README.md @@ -25,9 +25,12 @@ RustDesk welcomes contribution from everyone. See [CONTRIBUTING.md](docs/CONTRIB [**NIGHTLY BUILD**](https://github.com/rustdesk/rustdesk/releases/tag/nightly) -[Get it on F-Droid](https://f-droid.org/en/packages/com.carriez.flutter_hbb) +[Get it on Flathub](https://flathub.org/apps/com.rustdesk.RustDesk) ## Dependencies From e5aa31eb4ce960aed3b7d6318457dacd1d3eb82b Mon Sep 17 00:00:00 2001 From: 21pages Date: Mon, 16 Dec 2024 17:13:48 +0800 Subject: [PATCH 478/541] Fix auto record outgoing sessions ignore record permission (#10294) 1. Fix auto record outgoing sessions ignore record permission 2. Stop record if record permission changed 3. Update hwcodec 4. Make video thread finish faster when connection closed Signed-off-by: 21pages --- Cargo.lock | 2 +- src/client.rs | 15 +++++----- src/client/io_loop.rs | 57 +++++++++++++++++++++++++++++-------- src/flutter_ffi.rs | 7 ++--- src/ui_session_interface.rs | 7 +---- 5 files changed, 57 insertions(+), 31 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 61e6767a34f7..8270aa2caa32 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3065,7 +3065,7 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hwcodec" version = "0.7.1" -source = "git+https://github.com/rustdesk-org/hwcodec#7ee119a58b6ee6ca255a438af69ad0785ba44797" +source = "git+https://github.com/rustdesk-org/hwcodec#6376b41da010b87ae984e843cedca2b52c79b1cd" dependencies = [ "bindgen 0.59.2", "cc", diff --git a/src/client.rs b/src/client.rs index a201336ac0fb..83b6de43a376 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1384,7 +1384,8 @@ pub struct LoginConfigHandler { password_source: PasswordSource, // where the sent password comes from shared_password: Option, // Store the shared password pub enable_trusted_devices: bool, - pub record: bool, + pub record_state: bool, + pub record_permission: bool, } impl Deref for LoginConfigHandler { @@ -1489,7 +1490,8 @@ impl LoginConfigHandler { self.adapter_luid = adapter_luid; self.selected_windows_session_id = None; self.shared_password = shared_password; - self.record = LocalConfig::get_bool_option(OPTION_ALLOW_AUTO_RECORD_OUTGOING); + self.record_state = false; + self.record_permission = true; } /// Check if the client should auto login. @@ -2354,10 +2356,11 @@ pub fn start_video_thread( let format = CodecFormat::from(&vf); if video_handler.is_none() { let mut handler = VideoHandler::new(format, display); - let record = session.lc.read().unwrap().record; + let record_state = session.lc.read().unwrap().record_state; + let record_permission = session.lc.read().unwrap().record_permission; let id = session.lc.read().unwrap().id.clone(); - if record { - handler.record_screen(record, id, display); + if record_state && record_permission { + handler.record_screen(true, id, display); } video_handler = Some(handler); } @@ -2436,8 +2439,6 @@ pub fn start_video_thread( } } MediaData::RecordScreen(start) => { - log::info!("record screen command: start: {start}"); - session.update_record_status(start); let id = session.lc.read().unwrap().id.clone(); if let Some(handler) = video_handler.as_mut() { handler.record_screen(start, id, display); diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index df07331cfeac..cbef0d9140f3 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -27,7 +27,7 @@ use crossbeam_queue::ArrayQueue; use hbb_common::tokio::sync::mpsc::error::TryRecvError; use hbb_common::{ allow_err, - config::{self, PeerConfig, TransferSerde}, + config::{self, LocalConfig, PeerConfig, TransferSerde}, fs::{ self, can_enable_overwrite_detection, get_job, get_string, new_send_confirm, DigestCheckResult, RemoveJobMeta, @@ -71,6 +71,7 @@ pub struct Remote { peer_info: ParsedPeerInfo, video_threads: HashMap, chroma: Arc>>, + last_record_state: bool, } #[derive(Default)] @@ -116,6 +117,7 @@ impl Remote { peer_info: Default::default(), video_threads: Default::default(), chroma: Default::default(), + last_record_state: false, } } @@ -846,10 +848,8 @@ impl Remote { } } Data::RecordScreen(start) => { - self.handler.lc.write().unwrap().record = start; - for (_, v) in self.video_threads.iter_mut() { - v.video_sender.send(MediaData::RecordScreen(start)).ok(); - } + self.handler.lc.write().unwrap().record_state = start; + self.update_record_state(); } Data::ElevateDirect => { let mut request = ElevationRequest::new(); @@ -1484,6 +1484,8 @@ impl Remote { self.handler.set_permission("restart", p.enabled); } Ok(Permission::Recording) => { + self.handler.lc.write().unwrap().record_permission = p.enabled; + self.update_record_state(); self.handler.set_permission("recording", p.enabled); } Ok(Permission::BlockInput) => { @@ -1983,15 +1985,39 @@ impl Remote { }, ); self.video_threads.insert(display, video_thread); - let auto_record = self.handler.lc.read().unwrap().record; - if auto_record && self.video_threads.len() == 1 { - let mut misc = Misc::new(); - misc.set_client_record_status(true); - let mut msg = Message::new(); - msg.set_misc(misc); - self.sender.send(Data::Message(msg)).ok(); + if self.video_threads.len() == 1 { + let auto_record = + LocalConfig::get_bool_option(config::keys::OPTION_ALLOW_AUTO_RECORD_OUTGOING); + self.handler.lc.write().unwrap().record_state = auto_record; + self.update_record_state(); } } + + fn update_record_state(&mut self) { + // state + let permission = self.handler.lc.read().unwrap().record_permission; + if !permission { + self.handler.lc.write().unwrap().record_state = false; + } + let state = self.handler.lc.read().unwrap().record_state; + let start = state && permission; + if self.last_record_state == start { + return; + } + self.last_record_state = start; + log::info!("record screen start: {start}"); + // update local + for (_, v) in self.video_threads.iter_mut() { + v.video_sender.send(MediaData::RecordScreen(start)).ok(); + } + self.handler.update_record_status(start); + // update remote + let mut misc = Misc::new(); + misc.set_client_record_status(start); + let mut msg = Message::new(); + msg.set_misc(misc); + self.sender.send(Data::Message(msg)).ok(); + } } struct RemoveJob { @@ -2040,3 +2066,10 @@ struct VideoThread { discard_queue: Arc>, fps_control: FpsControl, } + +impl Drop for VideoThread { + fn drop(&mut self) { + // since channels are buffered, messages sent before the disconnect will still be properly received. + *self.discard_queue.write().unwrap() = true; + } +} diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 0bb17c9036dc..9e23b7b02674 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -1,3 +1,5 @@ +#[cfg(not(any(target_os = "android", target_os = "ios")))] +use crate::keyboard::input_source::{change_input_source, get_cur_session_input_source}; use crate::{ client::file_trait::FileManager, common::{make_fd_to_json, make_vec_fd_to_json}, @@ -7,11 +9,6 @@ use crate::{ input::*, ui_interface::{self, *}, }; -#[cfg(not(any(target_os = "android", target_os = "ios")))] -use crate::{ - common::get_default_sound_input, - keyboard::input_source::{change_input_source, get_cur_session_input_source}, -}; use flutter_rust_bridge::{StreamSink, SyncReturn}; #[cfg(feature = "plugin_framework")] #[cfg(not(any(target_os = "android", target_os = "ios")))] diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 176426464149..14baa69468b6 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -390,16 +390,11 @@ impl Session { } pub fn record_screen(&self, start: bool) { - let mut misc = Misc::new(); - misc.set_client_record_status(start); - let mut msg = Message::new(); - msg.set_misc(misc); - self.send(Data::Message(msg)); self.send(Data::RecordScreen(start)); } pub fn is_recording(&self) -> bool { - self.lc.read().unwrap().record + self.lc.read().unwrap().record_state } pub fn save_custom_image_quality(&self, custom_image_quality: i32) { From d025ca1d81a0c9230d452f9e6c7093dbd8ef0995 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Mon, 16 Dec 2024 19:01:12 +0800 Subject: [PATCH 479/541] refact: linux, chcon, bin_t (#10293) Signed-off-by: fufesou --- res/rpm-flutter.spec | 9 +++++++++ res/rpm.spec | 9 +++++++++ 2 files changed, 18 insertions(+) diff --git a/res/rpm-flutter.spec b/res/rpm-flutter.spec index 8862614ea32e..da37374f23c9 100644 --- a/res/rpm-flutter.spec +++ b/res/rpm-flutter.spec @@ -59,6 +59,15 @@ cp /usr/share/rustdesk/files/rustdesk.service /etc/systemd/system/rustdesk.servi cp /usr/share/rustdesk/files/rustdesk.desktop /usr/share/applications/ cp /usr/share/rustdesk/files/rustdesk-link.desktop /usr/share/applications/ ln -s /usr/lib/rustdesk/rustdesk /usr/bin/rustdesk +# Change the security context of /usr/lib/rustdesk/rustdesk from `lib_t` to `bin_t`. +if command -v getenforce >/dev/null 2>&1; then + if [ "$(getenforce)" == "Enforcing" ]; then + file_security_context=$(ls -lZ /usr/lib/rustdesk/rustdesk 2>/dev/null | awk -F':' '{print $3}') + if [ "${file_security_context}" == "lib_t" ]; then + chcon -t bin_t /usr/lib/rustdesk/rustdesk || true + fi + fi +fi systemctl daemon-reload systemctl enable rustdesk systemctl start rustdesk diff --git a/res/rpm.spec b/res/rpm.spec index 8d204eef2794..23bb71c54577 100644 --- a/res/rpm.spec +++ b/res/rpm.spec @@ -63,6 +63,15 @@ esac cp /usr/share/rustdesk/files/rustdesk.service /etc/systemd/system/rustdesk.service cp /usr/share/rustdesk/files/rustdesk.desktop /usr/share/applications/ cp /usr/share/rustdesk/files/rustdesk-link.desktop /usr/share/applications/ +# Change the security context of /usr/lib/rustdesk/rustdesk from `lib_t` to `bin_t`. +if command -v getenforce >/dev/null 2>&1; then + if [ "$(getenforce)" == "Enforcing" ]; then + file_security_context=$(ls -lZ /usr/lib/rustdesk/rustdesk 2>/dev/null | awk -F':' '{print $3}') + if [ "${file_security_context}" == "lib_t" ]; then + chcon -t bin_t /usr/lib/rustdesk/rustdesk || true + fi + fi +fi systemctl daemon-reload systemctl enable rustdesk systemctl start rustdesk From acae6d6558a7e5d84893c94a2d7e624728c18110 Mon Sep 17 00:00:00 2001 From: 21pages Date: Mon, 16 Dec 2024 19:40:48 +0800 Subject: [PATCH 480/541] try fix FFmpeg amf encode hang (#10283) * Possible Causes * GPU API Call Hangs: This could occur, though it's less likely. * Infinite Loop: If `QueryOutput` always fails and `hwsurfaces_in_queue_max` is zero, the loop will continue indefinitely. * Proposed Solution * A query_timeout patch has been added to FFmpeg with a value of 1000ms, which exceeds the time required to encode a single frame. This allows us to remove the loop. * Test * After removing the loop, no frame encoding failures were encountered during testing. A single call to QueryOutput is sufficient, as it typically consumes about 12ms on a 2K screen. Signed-off-by: 21pages --- .../patch/0008-remove-amf-loop-query.patch | 26 +++++++++++++++++++ res/vcpkg/ffmpeg/portfile.cmake | 1 + 2 files changed, 27 insertions(+) create mode 100644 res/vcpkg/ffmpeg/patch/0008-remove-amf-loop-query.patch diff --git a/res/vcpkg/ffmpeg/patch/0008-remove-amf-loop-query.patch b/res/vcpkg/ffmpeg/patch/0008-remove-amf-loop-query.patch new file mode 100644 index 000000000000..fe08aebdadf9 --- /dev/null +++ b/res/vcpkg/ffmpeg/patch/0008-remove-amf-loop-query.patch @@ -0,0 +1,26 @@ +From 1440f556234d135ce58a2ef38916c6a63b05870e Mon Sep 17 00:00:00 2001 +From: 21pages +Date: Sat, 14 Dec 2024 21:39:44 +0800 +Subject: [PATCH] remove amf loop query + +Signed-off-by: 21pages +--- + libavcodec/amfenc.c | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/libavcodec/amfenc.c b/libavcodec/amfenc.c +index f70f0109f6..a53a05b16b 100644 +--- a/libavcodec/amfenc.c ++++ b/libavcodec/amfenc.c +@@ -886,7 +886,7 @@ int ff_amf_receive_packet(AVCodecContext *avctx, AVPacket *avpkt) + av_usleep(1000); + } + } +- } while (block_and_wait); ++ } while (false); // already set query timeout + + if (res_query == AMF_EOF) { + ret = AVERROR_EOF; +-- +2.43.0.windows.1 + diff --git a/res/vcpkg/ffmpeg/portfile.cmake b/res/vcpkg/ffmpeg/portfile.cmake index e7a530ee6465..b8e05e211cbd 100644 --- a/res/vcpkg/ffmpeg/portfile.cmake +++ b/res/vcpkg/ffmpeg/portfile.cmake @@ -23,6 +23,7 @@ vcpkg_from_github( patch/0005-mediacodec-changing-bitrate.patch patch/0006-dlopen-libva.patch patch/0007-fix-linux-configure.patch + patch/0008-remove-amf-loop-query.patch ) if(SOURCE_PATH MATCHES " ") From 10ff3e69372547503361094ffb5abf4a6c559913 Mon Sep 17 00:00:00 2001 From: princeyogesh Date: Tue, 17 Dec 2024 08:07:57 +0530 Subject: [PATCH 481/541] Fix for compilation due to minimum Cmake version update and arm based compilation of vcpkg (#10297) --- Dockerfile | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 8544219c2b1c..f0e4e4a4a622 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,6 +2,7 @@ FROM debian:bullseye-slim WORKDIR / ARG DEBIAN_FRONTEND=noninteractive +ENV VCPKG_FORCE_SYSTEM_BINARIES=1 RUN apt update -y && \ apt install --yes --no-install-recommends \ g++ \ @@ -21,7 +22,8 @@ RUN apt update -y && \ libpam0g-dev \ libpulse-dev \ make \ - cmake \ + wget \ + libssl-dev \ unzip \ zip \ sudo \ @@ -31,6 +33,13 @@ RUN apt update -y && \ ninja-build && \ rm -rf /var/lib/apt/lists/* +RUN wget https://github.com/Kitware/CMake/releases/download/v3.30.6/cmake-3.30.6.tar.gz --no-check-certificate && \ + tar xzf cmake-3.30.6.tar.gz && \ + cd cmake-3.30.6 && \ + ./configure --prefix=/usr/local && \ + make && \ + make install + RUN git clone --branch 2023.04.15 --depth=1 https://github.com/microsoft/vcpkg && \ /vcpkg/bootstrap-vcpkg.sh -disableMetrics && \ /vcpkg/vcpkg --disable-metrics install libvpx libyuv opus aom From e163b75407ccb53c59d2274136b6db1055abd2af Mon Sep 17 00:00:00 2001 From: 21pages Date: Tue, 17 Dec 2024 12:07:34 +0800 Subject: [PATCH 482/541] update hwcodec (#10304) Signed-off-by: 21pages --- Cargo.lock | 2 +- libs/scrap/src/common/hwcodec.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8270aa2caa32..6bce25667b83 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3065,7 +3065,7 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hwcodec" version = "0.7.1" -source = "git+https://github.com/rustdesk-org/hwcodec#6376b41da010b87ae984e843cedca2b52c79b1cd" +source = "git+https://github.com/rustdesk-org/hwcodec#81821d9138693a757f78b3412c35a36f797ec711" dependencies = [ "bindgen 0.59.2", "cc", diff --git a/libs/scrap/src/common/hwcodec.rs b/libs/scrap/src/common/hwcodec.rs index 3d56472eedc4..add4b73d4952 100644 --- a/libs/scrap/src/common/hwcodec.rs +++ b/libs/scrap/src/common/hwcodec.rs @@ -692,8 +692,8 @@ pub fn check_available_hwcodec() -> String { #[cfg(not(feature = "vram"))] let vram_string = "".to_owned(); let c = HwCodecConfig { - ram_encode: Encoder::available_encoders(ctx, Some(vram_string.clone())), - ram_decode: Decoder::available_decoders(Some(vram_string)), + ram_encode: Encoder::available_encoders(ctx, Some(vram_string)), + ram_decode: Decoder::available_decoders(), #[cfg(feature = "vram")] vram_encode: vram.0, #[cfg(feature = "vram")] From 9dd9c45afcc2274017f5220900ab5ac32a5c0a28 Mon Sep 17 00:00:00 2001 From: 21pages Date: Tue, 17 Dec 2024 15:01:01 +0800 Subject: [PATCH 483/541] fix ci (#10305) Signed-off-by: 21pages --- Cargo.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 6bce25667b83..fb876060d062 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3065,7 +3065,7 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hwcodec" version = "0.7.1" -source = "git+https://github.com/rustdesk-org/hwcodec#81821d9138693a757f78b3412c35a36f797ec711" +source = "git+https://github.com/rustdesk-org/hwcodec#dae7859f0d46e539d0a1e3b7fe74b0450398f149" dependencies = [ "bindgen 0.59.2", "cc", From e4b270a581446b1b54f2c3a562bae6fca484cc3e Mon Sep 17 00:00:00 2001 From: 21pages Date: Tue, 17 Dec 2024 21:52:17 +0800 Subject: [PATCH 484/541] update hwcodec (#10306) Signed-off-by: 21pages --- Cargo.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index fb876060d062..dc5de1d4057d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3065,7 +3065,7 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hwcodec" version = "0.7.1" -source = "git+https://github.com/rustdesk-org/hwcodec#dae7859f0d46e539d0a1e3b7fe74b0450398f149" +source = "git+https://github.com/rustdesk-org/hwcodec#3e7c0dc755f8a77bbed3b2a9921553a511fd7bb5" dependencies = [ "bindgen 0.59.2", "cc", From ed9cb372834f1c3b04552907296555a1e664ce31 Mon Sep 17 00:00:00 2001 From: Iacopo Modica <52070747+n3ural@users.noreply.github.com> Date: Wed, 18 Dec 2024 14:02:53 +0100 Subject: [PATCH 485/541] Fix translation issues in Italian language file (#10312) Corrected multiple translation errors and typos in the Italian language resource file. --- src/lang/it.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/lang/it.rs b/src/lang/it.rs index 46d4c937dc5c..0f6657bbc349 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -31,8 +31,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("ID/Relay Server", "Server ID/Relay"), ("Import server config", "Importa configurazione server dagli appunti"), ("Export Server Config", "Esporta configurazione server negli appunti"), - ("Import server configuration successfully", "Configurazione server importata completata"), - ("Export server configuration successfully", "Configurazione Server esportata completata"), + ("Import server configuration successfully", "Configurazione server importata con successo"), + ("Export server configuration successfully", "Configurazione Server esportata con successo"), ("Invalid server configuration", "Configurazione server non valida"), ("Clipboard is empty", "Gli appunti sono vuoti"), ("Stop service", "Arresta servizio"), @@ -105,7 +105,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Are you sure you want to delete this file?", "Sei sicuro di voler eliminare questo file?"), ("Are you sure you want to delete this empty directory?", "Sei sicuro di voler eliminare questa cartella vuota?"), ("Are you sure you want to delete the file of this directory?", "Sei sicuro di voler eliminare il file di questa cartella?"), - ("Do this for all conflicts", "Ricorca questa scelta per tutti i conflitti"), + ("Do this for all conflicts", "Ricorda questa scelta per tutti i conflitti"), ("This is irreversible!", "Questo è irreversibile!"), ("Deleting", "Eliminazione di"), ("files", "file"), @@ -226,7 +226,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add ID", "Aggiungi ID"), ("Add Tag", "Aggiungi etichetta"), ("Unselect all tags", "Deseleziona tutte le etichette"), - ("Network error", "Errore rete"), + ("Network error", "Errore di rete"), ("Username missed", "Nome utente mancante"), ("Password missed", "Password mancante"), ("Wrong credentials", "Credenziali errate"), @@ -297,7 +297,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Succeeded", "Completato"), ("Someone turns on privacy mode, exit", "Qualcuno ha attivato la modalità privacy, uscita"), ("Unsupported", "Non supportato"), - ("Peer denied", "Acvesso negato al dispositivo remoto"), + ("Peer denied", "Accesso negato al dispositivo remoto"), ("Please install plugins", "Installa i plugin"), ("Peer exit", "Uscita dal dispostivo remoto"), ("Failed to turn off", "Impossibile spegnere"), @@ -479,7 +479,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("another_user_login_text_tip", "Separato"), ("xorg_not_found_title_tip", "Xorg non trovato."), ("xorg_not_found_text_tip", "Installa Xorg."), - ("no_desktop_title_tip", "Non c'è nessun envorinment desktop disponibile."), + ("no_desktop_title_tip", "Non è presente alcun ambiente desktop disponibile."), ("no_desktop_text_tip", "Installa il desktop GNOME."), ("No need to elevate", "Elevazione dei privilegi non richiesta"), ("System Sound", "Dispositivo audio sistema"), @@ -625,7 +625,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Volume up", "Volume +"), ("Volume down", "Volume -"), ("Power", "Alimentazione"), - ("Telegram bot", "Bot Telgram"), + ("Telegram bot", "Bot Telegram"), ("enable-bot-tip", "Se abiliti questa funzione, puoi ricevere il codice 2FA dal tuo bot.\nPuò anche funzionare come notifica di connessione."), ("enable-bot-desc", "1. apri una chat con @BotFather.\n2. Invia il comando \"/newbot\", dopo aver completato questo passaggio riceverai un token.\n3. Avvia una chat con il tuo bot appena creato. Per attivarlo Invia un messaggio che inizia con una barra (\"/\") tipo \"/hello\".\n"), ("cancel-2fa-confirm-tip", "Sei sicuro di voler annullare 2FA?"), From 5fa848513058f89a34cff2b9ef9a96ffbc4a85bc Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Wed, 18 Dec 2024 21:17:04 +0800 Subject: [PATCH 486/541] fix: macos, show remote cursor (#10314) Signed-off-by: fufesou --- Cargo.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index dc5de1d4057d..3fc53573b4e1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1290,7 +1290,7 @@ dependencies = [ [[package]] name = "cpal" version = "0.15.3" -source = "git+https://github.com/rustdesk-org/cpal?branch=osx-screencapturekit#7cb4ed0bd5546bf209edde0d0e9da5194753f2c0" +source = "git+https://github.com/rustdesk-org/cpal?branch=osx-screencapturekit#6b374bcaed076750ca8fce6da518ab39b882e14a" dependencies = [ "alsa", "cidre", From 7830a9e9f3334b2af3bf10656f6190bc10a35504 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Thu, 19 Dec 2024 15:05:24 +0800 Subject: [PATCH 487/541] refact: linux, install path (#10316) Signed-off-by: fufesou --- appimage/AppImageBuilder-aarch64.yml | 4 ++-- appimage/AppImageBuilder-x86_64.yml | 4 ++-- build.py | 16 ++++++++-------- flatpak/rustdesk.json | 2 +- res/DEBIAN/postinst | 2 +- res/PKGBUILD | 4 ++-- res/rpm-flutter-suse.spec | 10 ++++++---- res/rpm-flutter.spec | 19 ++++++------------- res/rpm-suse.spec | 6 +++--- res/rpm.spec | 15 +++------------ src/ui.rs | 4 ++-- 11 files changed, 36 insertions(+), 50 deletions(-) diff --git a/appimage/AppImageBuilder-aarch64.yml b/appimage/AppImageBuilder-aarch64.yml index 98ceca3bb5ea..9f8490ac99e1 100644 --- a/appimage/AppImageBuilder-aarch64.yml +++ b/appimage/AppImageBuilder-aarch64.yml @@ -19,7 +19,7 @@ AppDir: name: rustdesk icon: rustdesk version: 1.3.5 - exec: usr/lib/rustdesk/rustdesk + exec: usr/local/rustdesk/rustdesk exec_args: $@ apt: arch: @@ -77,7 +77,7 @@ AppDir: env: GIO_MODULE_DIR: /lib64/gio/modules:/usr/lib/aarch64-linux-gnu/gio/modules:$APPDIR/usr/lib/aarch64-linux-gnu/gio/modules 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 + 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/local/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: diff --git a/appimage/AppImageBuilder-x86_64.yml b/appimage/AppImageBuilder-x86_64.yml index 9ce7cc717e33..8a5f942341c3 100644 --- a/appimage/AppImageBuilder-x86_64.yml +++ b/appimage/AppImageBuilder-x86_64.yml @@ -19,7 +19,7 @@ AppDir: name: rustdesk icon: rustdesk version: 1.3.5 - exec: usr/lib/rustdesk/rustdesk + exec: usr/local/rustdesk/rustdesk exec_args: $@ apt: arch: @@ -80,7 +80,7 @@ AppDir: env: 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 - 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 + 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/local/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: diff --git a/build.py b/build.py index 5d9740920375..802d6a7a69f6 100755 --- a/build.py +++ b/build.py @@ -321,7 +321,7 @@ def build_flutter_deb(version, features): os.chdir('flutter') system2('flutter build linux --release') system2('mkdir -p tmpdeb/usr/bin/') - system2('mkdir -p tmpdeb/usr/lib/rustdesk') + system2('mkdir -p tmpdeb/usr/local/rustdesk') system2('mkdir -p tmpdeb/etc/rustdesk/') system2('mkdir -p tmpdeb/etc/pam.d/') system2('mkdir -p tmpdeb/usr/share/rustdesk/files/systemd/') @@ -331,7 +331,7 @@ def build_flutter_deb(version, features): system2('mkdir -p tmpdeb/usr/share/polkit-1/actions') system2('rm tmpdeb/usr/bin/rustdesk || true') system2( - f'cp -r {flutter_build_dir}/* tmpdeb/usr/lib/rustdesk/') + f'cp -r {flutter_build_dir}/* tmpdeb/usr/local/rustdesk/') system2( 'cp ../res/rustdesk.service tmpdeb/usr/share/rustdesk/files/systemd/') system2( @@ -366,7 +366,7 @@ def build_flutter_deb(version, features): def build_deb_from_folder(version, binary_folder): os.chdir('flutter') system2('mkdir -p tmpdeb/usr/bin/') - system2('mkdir -p tmpdeb/usr/lib/rustdesk') + system2('mkdir -p tmpdeb/usr/local/rustdesk') system2('mkdir -p tmpdeb/usr/share/rustdesk/files/systemd/') system2('mkdir -p tmpdeb/usr/share/icons/hicolor/256x256/apps/') system2('mkdir -p tmpdeb/usr/share/icons/hicolor/scalable/apps/') @@ -374,7 +374,7 @@ def build_deb_from_folder(version, binary_folder): system2('mkdir -p tmpdeb/usr/share/polkit-1/actions') system2('rm tmpdeb/usr/bin/rustdesk || true') system2( - f'cp -r ../{binary_folder}/* tmpdeb/usr/lib/rustdesk/') + f'cp -r ../{binary_folder}/* tmpdeb/usr/local/rustdesk/') system2( 'cp ../res/rustdesk.service tmpdeb/usr/share/rustdesk/files/systemd/') system2( @@ -621,14 +621,14 @@ def main(): os.system('mkdir -p tmpdeb/etc/pam.d/') os.system('cp pam.d/rustdesk.debian tmpdeb/etc/pam.d/rustdesk') system2('strip tmpdeb/usr/bin/rustdesk') - system2('mkdir -p tmpdeb/usr/lib/rustdesk') - system2('mv tmpdeb/usr/bin/rustdesk tmpdeb/usr/lib/rustdesk/') - system2('cp libsciter-gtk.so tmpdeb/usr/lib/rustdesk/') + system2('mkdir -p tmpdeb/usr/local/rustdesk') + system2('mv tmpdeb/usr/bin/rustdesk tmpdeb/usr/local/rustdesk/') + system2('cp libsciter-gtk.so tmpdeb/usr/local/rustdesk/') md5_file('usr/share/rustdesk/files/systemd/rustdesk.service') md5_file('etc/rustdesk/startwm.sh') md5_file('etc/X11/rustdesk/xorg.conf') md5_file('etc/pam.d/rustdesk') - md5_file('usr/lib/rustdesk/libsciter-gtk.so') + md5_file('usr/local/rustdesk/libsciter-gtk.so') system2('dpkg-deb -b tmpdeb rustdesk.deb; /bin/rm -rf tmpdeb/') os.rename('rustdesk.deb', 'rustdesk-%s.deb' % version) diff --git a/flatpak/rustdesk.json b/flatpak/rustdesk.json index 57a2f158ad49..2aab79aea62d 100644 --- a/flatpak/rustdesk.json +++ b/flatpak/rustdesk.json @@ -39,7 +39,7 @@ "build-commands": [ "bsdtar -Oxf rustdesk.deb data.tar.xz | bsdtar -xf -", "cp -r usr/* /app/", - "mkdir -p /app/bin && ln -s /app/lib/rustdesk/rustdesk /app/bin/rustdesk" + "mkdir -p /app/bin && ln -s /app/local/rustdesk/rustdesk /app/bin/rustdesk" ], "sources": [ { diff --git a/res/DEBIAN/postinst b/res/DEBIAN/postinst index eeeccaaec8be..07956f11fa34 100755 --- a/res/DEBIAN/postinst +++ b/res/DEBIAN/postinst @@ -5,7 +5,7 @@ set -e if [ "$1" = configure ]; then INITSYS=$(ls -al /proc/1/exe | awk -F' ' '{print $NF}' | awk -F'/' '{print $NF}') - ln -s /usr/lib/rustdesk/rustdesk /usr/bin/rustdesk + ln -s /usr/local/rustdesk/rustdesk /usr/bin/rustdesk if [ "systemd" == "$INITSYS" ]; then diff --git a/res/PKGBUILD b/res/PKGBUILD index 79061a41b05c..49a5a4cadf98 100644 --- a/res/PKGBUILD +++ b/res/PKGBUILD @@ -23,10 +23,10 @@ md5sums=() #generate with 'makepkg -g' package() { if [[ ${FLUTTER} ]]; then - mkdir -p "${pkgdir}/usr/lib/rustdesk" && cp -r ${HBB}/flutter/build/linux/x64/release/bundle/* -t "${pkgdir}/usr/lib/rustdesk" + mkdir -p "${pkgdir}/usr/local/rustdesk" && cp -r ${HBB}/flutter/build/linux/x64/release/bundle/* -t "${pkgdir}/usr/local/rustdesk" fi mkdir -p "${pkgdir}/usr/bin" - pushd ${pkgdir} && ln -s /usr/lib/rustdesk/rustdesk usr/bin/rustdesk && popd + pushd ${pkgdir} && ln -s /usr/local/rustdesk/rustdesk usr/bin/rustdesk && popd install -Dm 644 $HBB/res/rustdesk.service -t "${pkgdir}/usr/share/rustdesk/files" install -Dm 644 $HBB/res/rustdesk.desktop -t "${pkgdir}/usr/share/rustdesk/files" install -Dm 644 $HBB/res/rustdesk-link.desktop -t "${pkgdir}/usr/share/rustdesk/files" diff --git a/res/rpm-flutter-suse.spec b/res/rpm-flutter-suse.spec index 5686eea7abfa..7d0178be2fd7 100644 --- a/res/rpm-flutter-suse.spec +++ b/res/rpm-flutter-suse.spec @@ -22,7 +22,7 @@ The best open-source remote desktop client software, written in Rust. %install -mkdir -p "%{buildroot}/usr/lib/rustdesk" && cp -r ${HBB}/flutter/build/linux/x64/release/bundle/* -t "%{buildroot}/usr/lib/rustdesk" +mkdir -p "%{buildroot}/usr/local/rustdesk" && cp -r ${HBB}/flutter/build/linux/x64/release/bundle/* -t "%{buildroot}/usr/local/rustdesk" mkdir -p "%{buildroot}/usr/bin" install -Dm 644 $HBB/res/rustdesk.service -t "%{buildroot}/usr/share/rustdesk/files" install -Dm 644 $HBB/res/rustdesk.desktop -t "%{buildroot}/usr/share/rustdesk/files" @@ -31,7 +31,7 @@ install -Dm 644 $HBB/res/128x128@2x.png "%{buildroot}/usr/share/icons/hicolor/25 install -Dm 644 $HBB/res/scalable.svg "%{buildroot}/usr/share/icons/hicolor/scalable/apps/rustdesk.svg" %files -/usr/lib/rustdesk/* +/usr/local/rustdesk/* /usr/share/rustdesk/files/rustdesk.service /usr/share/icons/hicolor/256x256/apps/rustdesk.png /usr/share/icons/hicolor/scalable/apps/rustdesk.svg @@ -58,7 +58,7 @@ esac cp /usr/share/rustdesk/files/rustdesk.service /etc/systemd/system/rustdesk.service cp /usr/share/rustdesk/files/rustdesk.desktop /usr/share/applications/ cp /usr/share/rustdesk/files/rustdesk-link.desktop /usr/share/applications/ -ln -s /usr/lib/rustdesk/rustdesk /usr/bin/rustdesk +ln -sf /usr/local/rustdesk/rustdesk /usr/bin/rustdesk systemctl daemon-reload systemctl enable rustdesk systemctl start rustdesk @@ -81,9 +81,11 @@ esac case "$1" in 0) # for uninstall + rm /usr/bin/rustdesk || true + rmdir /usr/lib/rustdesk || true + rmdir /usr/local/rustdesk || true rm /usr/share/applications/rustdesk.desktop || true rm /usr/share/applications/rustdesk-link.desktop || true - rm /usr/bin/rustdesk || true update-desktop-database ;; 1) diff --git a/res/rpm-flutter.spec b/res/rpm-flutter.spec index da37374f23c9..24fd2ab841ee 100644 --- a/res/rpm-flutter.spec +++ b/res/rpm-flutter.spec @@ -22,7 +22,7 @@ The best open-source remote desktop client software, written in Rust. %install -mkdir -p "%{buildroot}/usr/lib/rustdesk" && cp -r ${HBB}/flutter/build/linux/x64/release/bundle/* -t "%{buildroot}/usr/lib/rustdesk" +mkdir -p "%{buildroot}/usr/local/rustdesk" && cp -r ${HBB}/flutter/build/linux/x64/release/bundle/* -t "%{buildroot}/usr/local/rustdesk" mkdir -p "%{buildroot}/usr/bin" install -Dm 644 $HBB/res/rustdesk.service -t "%{buildroot}/usr/share/rustdesk/files" install -Dm 644 $HBB/res/rustdesk.desktop -t "%{buildroot}/usr/share/rustdesk/files" @@ -31,7 +31,7 @@ install -Dm 644 $HBB/res/128x128@2x.png "%{buildroot}/usr/share/icons/hicolor/25 install -Dm 644 $HBB/res/scalable.svg "%{buildroot}/usr/share/icons/hicolor/scalable/apps/rustdesk.svg" %files -/usr/lib/rustdesk/* +/usr/local/rustdesk/* /usr/share/rustdesk/files/rustdesk.service /usr/share/icons/hicolor/256x256/apps/rustdesk.png /usr/share/icons/hicolor/scalable/apps/rustdesk.svg @@ -58,16 +58,7 @@ esac cp /usr/share/rustdesk/files/rustdesk.service /etc/systemd/system/rustdesk.service cp /usr/share/rustdesk/files/rustdesk.desktop /usr/share/applications/ cp /usr/share/rustdesk/files/rustdesk-link.desktop /usr/share/applications/ -ln -s /usr/lib/rustdesk/rustdesk /usr/bin/rustdesk -# Change the security context of /usr/lib/rustdesk/rustdesk from `lib_t` to `bin_t`. -if command -v getenforce >/dev/null 2>&1; then - if [ "$(getenforce)" == "Enforcing" ]; then - file_security_context=$(ls -lZ /usr/lib/rustdesk/rustdesk 2>/dev/null | awk -F':' '{print $3}') - if [ "${file_security_context}" == "lib_t" ]; then - chcon -t bin_t /usr/lib/rustdesk/rustdesk || true - fi - fi -fi +ln -sf /usr/local/rustdesk/rustdesk /usr/bin/rustdesk systemctl daemon-reload systemctl enable rustdesk systemctl start rustdesk @@ -90,9 +81,11 @@ esac case "$1" in 0) # for uninstall + rm /usr/bin/rustdesk || true + rmdir /usr/lib/rustdesk || true + rmdir /usr/local/rustdesk || true rm /usr/share/applications/rustdesk.desktop || true rm /usr/share/applications/rustdesk-link.desktop || true - rm /usr/bin/rustdesk || true update-desktop-database ;; 1) diff --git a/res/rpm-suse.spec b/res/rpm-suse.spec index 46710e3c9d17..5686b74ef473 100644 --- a/res/rpm-suse.spec +++ b/res/rpm-suse.spec @@ -19,12 +19,12 @@ The best open-source remote desktop client software, written in Rust. %install mkdir -p %{buildroot}/usr/bin/ -mkdir -p %{buildroot}/usr/lib/rustdesk/ +mkdir -p %{buildroot}/usr/local/rustdesk/ mkdir -p %{buildroot}/usr/share/rustdesk/files/ mkdir -p %{buildroot}/usr/share/icons/hicolor/256x256/apps/ mkdir -p %{buildroot}/usr/share/icons/hicolor/scalable/apps/ install -m 755 $HBB/target/release/rustdesk %{buildroot}/usr/bin/rustdesk -install $HBB/libsciter-gtk.so %{buildroot}/usr/lib/rustdesk/libsciter-gtk.so +install $HBB/libsciter-gtk.so %{buildroot}/usr/local/rustdesk/libsciter-gtk.so install $HBB/res/rustdesk.service %{buildroot}/usr/share/rustdesk/files/ install $HBB/res/128x128@2x.png %{buildroot}/usr/share/icons/hicolor/256x256/apps/rustdesk.png install $HBB/res/scalable.svg %{buildroot}/usr/share/icons/hicolor/scalable/apps/rustdesk.svg @@ -33,7 +33,7 @@ install $HBB/res/rustdesk-link.desktop %{buildroot}/usr/share/rustdesk/files/ %files /usr/bin/rustdesk -/usr/lib/rustdesk/libsciter-gtk.so +/usr/local/rustdesk/libsciter-gtk.so /usr/share/rustdesk/files/rustdesk.service /usr/share/icons/hicolor/256x256/apps/rustdesk.png /usr/share/icons/hicolor/scalable/apps/rustdesk.svg diff --git a/res/rpm.spec b/res/rpm.spec index 23bb71c54577..874f83aa87a4 100644 --- a/res/rpm.spec +++ b/res/rpm.spec @@ -21,12 +21,12 @@ The best open-source remote desktop client software, written in Rust. %install mkdir -p %{buildroot}/usr/bin/ -mkdir -p %{buildroot}/usr/lib/rustdesk/ +mkdir -p %{buildroot}/usr/local/rustdesk/ mkdir -p %{buildroot}/usr/share/rustdesk/files/ mkdir -p %{buildroot}/usr/share/icons/hicolor/256x256/apps/ mkdir -p %{buildroot}/usr/share/icons/hicolor/scalable/apps/ install -m 755 $HBB/target/release/rustdesk %{buildroot}/usr/bin/rustdesk -install $HBB/libsciter-gtk.so %{buildroot}/usr/lib/rustdesk/libsciter-gtk.so +install $HBB/libsciter-gtk.so %{buildroot}/usr/local/rustdesk/libsciter-gtk.so install $HBB/res/rustdesk.service %{buildroot}/usr/share/rustdesk/files/ install $HBB/res/128x128@2x.png %{buildroot}/usr/share/icons/hicolor/256x256/apps/rustdesk.png install $HBB/res/scalable.svg %{buildroot}/usr/share/icons/hicolor/scalable/apps/rustdesk.svg @@ -35,7 +35,7 @@ install $HBB/res/rustdesk-link.desktop %{buildroot}/usr/share/rustdesk/files/ %files /usr/bin/rustdesk -/usr/lib/rustdesk/libsciter-gtk.so +/usr/local/rustdesk/libsciter-gtk.so /usr/share/rustdesk/files/rustdesk.service /usr/share/icons/hicolor/256x256/apps/rustdesk.png /usr/share/icons/hicolor/scalable/apps/rustdesk.svg @@ -63,15 +63,6 @@ esac cp /usr/share/rustdesk/files/rustdesk.service /etc/systemd/system/rustdesk.service cp /usr/share/rustdesk/files/rustdesk.desktop /usr/share/applications/ cp /usr/share/rustdesk/files/rustdesk-link.desktop /usr/share/applications/ -# Change the security context of /usr/lib/rustdesk/rustdesk from `lib_t` to `bin_t`. -if command -v getenforce >/dev/null 2>&1; then - if [ "$(getenforce)" == "Enforcing" ]; then - file_security_context=$(ls -lZ /usr/lib/rustdesk/rustdesk 2>/dev/null | awk -F':' '{print $3}') - if [ "${file_security_context}" == "lib_t" ]; then - chcon -t bin_t /usr/lib/rustdesk/rustdesk || true - fi - fi -fi systemctl daemon-reload systemctl enable rustdesk systemctl start rustdesk diff --git a/src/ui.rs b/src/ui.rs index d3d291433ba8..79a2207e63b2 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -42,7 +42,7 @@ pub fn start(args: &mut [String]) { #[cfg(all(target_os = "linux", feature = "inline"))] { let app_dir = std::env::var("APPDIR").unwrap_or("".to_string()); - let mut so_path = "/usr/lib/rustdesk/libsciter-gtk.so".to_owned(); + let mut so_path = "/usr/local/rustdesk/libsciter-gtk.so".to_owned(); for (prefix, dir) in [ ("", "/usr"), ("", "/app"), @@ -51,7 +51,7 @@ pub fn start(args: &mut [String]) { ] .iter() { - let path = format!("{prefix}{dir}/lib/rustdesk/libsciter-gtk.so"); + let path = format!("{prefix}{dir}/local/rustdesk/libsciter-gtk.so"); if std::path::Path::new(&path).exists() { so_path = path; break; From 9114743577d31c4b77d622c17bb3df79a4a1b910 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Fri, 20 Dec 2024 09:24:08 +0800 Subject: [PATCH 488/541] fix: linux, flutter, workaround freeze (#10324) Signed-off-by: fufesou --- flutter/lib/common.dart | 13 +++ flutter/lib/common/widgets/address_book.dart | 10 +- flutter/lib/common/widgets/chat_page.dart | 2 +- flutter/lib/common/widgets/dialog.dart | 91 ++++++++++--------- flutter/lib/common/widgets/login.dart | 2 +- flutter/lib/common/widgets/my_group.dart | 2 +- flutter/lib/common/widgets/peer_card.dart | 6 +- flutter/lib/common/widgets/peer_tab_page.dart | 2 +- .../lib/desktop/pages/connection_page.dart | 2 +- .../lib/desktop/pages/desktop_home_page.dart | 8 +- .../desktop/pages/desktop_setting_page.dart | 12 +-- .../lib/desktop/pages/file_manager_page.dart | 4 +- flutter/lib/desktop/pages/install_page.dart | 2 +- .../lib/desktop/pages/port_forward_page.dart | 2 +- .../lib/desktop/widgets/remote_toolbar.dart | 4 +- .../lib/mobile/pages/file_manager_page.dart | 2 +- flutter/lib/mobile/pages/remote_page.dart | 2 +- flutter/lib/mobile/widgets/dialog.dart | 8 +- 18 files changed, 95 insertions(+), 79 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 208897ed0397..7f978201ae21 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -3625,3 +3625,16 @@ void checkUpdate() { } } } + +// https://github.com/flutter/flutter/issues/153560#issuecomment-2497160535 +// For TextField, TextFormField +extension WorkaroundFreezeLinuxMint on Widget { + Widget workaroundFreezeLinuxMint() { + // No need to check if is Linux Mint, because this workaround is harmless on other platforms. + if (isLinux) { + return ExcludeSemantics(child: this); + } else { + return this; + } + } +} diff --git a/flutter/lib/common/widgets/address_book.dart b/flutter/lib/common/widgets/address_book.dart index ae07c1498cf1..deed97bb30bd 100644 --- a/flutter/lib/common/widgets/address_book.dart +++ b/flutter/lib/common/widgets/address_book.dart @@ -286,7 +286,7 @@ class _AddressBookState extends State { borderRadius: BorderRadius.circular(8), ), ), - ), + ).workaroundFreezeLinuxMint(), ), searchMatchFn: (item, searchValue) { return item.value @@ -556,7 +556,7 @@ class _AddressBookState extends State { : translate('ID'), errorText: errorMsg, errorMaxLines: 5), - ))), + ).workaroundFreezeLinuxMint())), row( lable: Text( translate('Alias'), @@ -569,7 +569,7 @@ class _AddressBookState extends State { ? null : translate('Alias'), ), - )), + ).workaroundFreezeLinuxMint()), ), if (isCurrentAbShared) row( @@ -598,7 +598,7 @@ class _AddressBookState extends State { }, ), ), - ), + ).workaroundFreezeLinuxMint(), )), if (gFFI.abModel.currentAbTags.isNotEmpty) Align( @@ -704,7 +704,7 @@ class _AddressBookState extends State { ), controller: controller, autofocus: true, - ), + ).workaroundFreezeLinuxMint(), ), ], ), diff --git a/flutter/lib/common/widgets/chat_page.dart b/flutter/lib/common/widgets/chat_page.dart index b6611d3ede39..4b0954d40b19 100644 --- a/flutter/lib/common/widgets/chat_page.dart +++ b/flutter/lib/common/widgets/chat_page.dart @@ -167,7 +167,7 @@ class ChatPage extends StatelessWidget implements PageShape { ); }, ), - ); + ).workaroundFreezeLinuxMint(); return SelectionArea(child: chat); }), ], diff --git a/flutter/lib/common/widgets/dialog.dart b/flutter/lib/common/widgets/dialog.dart index cc3e0613105b..0fb8d552d7ab 100644 --- a/flutter/lib/common/widgets/dialog.dart +++ b/flutter/lib/common/widgets/dialog.dart @@ -140,7 +140,7 @@ void changeIdDialog() { msg = ''; }); }, - ), + ).workaroundFreezeLinuxMint(), const SizedBox( height: 8.0, ), @@ -201,13 +201,14 @@ void changeWhiteList({Function()? callback}) async { children: [ Expanded( child: TextField( - maxLines: null, - decoration: InputDecoration( - errorText: msg.isEmpty ? null : translate(msg), - ), - controller: controller, - enabled: !isOptFixed, - autofocus: true), + maxLines: null, + decoration: InputDecoration( + errorText: msg.isEmpty ? null : translate(msg), + ), + controller: controller, + enabled: !isOptFixed, + autofocus: true) + .workaroundFreezeLinuxMint(), ), ], ), @@ -287,22 +288,23 @@ Future changeDirectAccessPort( children: [ Expanded( child: TextField( - maxLines: null, - keyboardType: TextInputType.number, - decoration: InputDecoration( - hintText: '21118', - isCollapsed: true, - prefix: Text('$currentIP : '), - suffix: IconButton( - padding: EdgeInsets.zero, - icon: const Icon(Icons.clear, size: 16), - onPressed: () => controller.clear())), - inputFormatters: [ - FilteringTextInputFormatter.allow(RegExp( - r'^([0-9]|[1-9]\d|[1-9]\d{2}|[1-9]\d{3}|[1-5]\d{4}|6[0-4]\d{3}|65[0-4]\d{2}|655[0-2]\d|6553[0-5])$')), - ], - controller: controller, - autofocus: true), + maxLines: null, + keyboardType: TextInputType.number, + decoration: InputDecoration( + hintText: '21118', + isCollapsed: true, + prefix: Text('$currentIP : '), + suffix: IconButton( + padding: EdgeInsets.zero, + icon: const Icon(Icons.clear, size: 16), + onPressed: () => controller.clear())), + inputFormatters: [ + FilteringTextInputFormatter.allow(RegExp( + r'^([0-9]|[1-9]\d|[1-9]\d{2}|[1-9]\d{3}|[1-5]\d{4}|6[0-4]\d{3}|65[0-4]\d{2}|655[0-2]\d|6553[0-5])$')), + ], + controller: controller, + autofocus: true) + .workaroundFreezeLinuxMint(), ), ], ), @@ -335,21 +337,22 @@ Future changeAutoDisconnectTimeout(String old) async { children: [ Expanded( child: TextField( - maxLines: null, - keyboardType: TextInputType.number, - decoration: InputDecoration( - hintText: '10', - isCollapsed: true, - suffix: IconButton( - padding: EdgeInsets.zero, - icon: const Icon(Icons.clear, size: 16), - onPressed: () => controller.clear())), - inputFormatters: [ - FilteringTextInputFormatter.allow(RegExp( - r'^([0-9]|[1-9]\d|[1-9]\d{2}|[1-9]\d{3}|[1-5]\d{4}|6[0-4]\d{3}|65[0-4]\d{2}|655[0-2]\d|6553[0-5])$')), - ], - controller: controller, - autofocus: true), + maxLines: null, + keyboardType: TextInputType.number, + decoration: InputDecoration( + hintText: '10', + isCollapsed: true, + suffix: IconButton( + padding: EdgeInsets.zero, + icon: const Icon(Icons.clear, size: 16), + onPressed: () => controller.clear())), + inputFormatters: [ + FilteringTextInputFormatter.allow(RegExp( + r'^([0-9]|[1-9]\d|[1-9]\d{2}|[1-9]\d{3}|[1-5]\d{4}|6[0-4]\d{3}|65[0-4]\d{2}|655[0-2]\d|6553[0-5])$')), + ], + controller: controller, + autofocus: true) + .workaroundFreezeLinuxMint(), ), ], ), @@ -427,7 +430,7 @@ class DialogTextField extends StatelessWidget { keyboardType: keyboardType, inputFormatters: inputFormatters, maxLength: maxLength, - ), + ).workaroundFreezeLinuxMint(), ), ], ).paddingSymmetric(vertical: 4.0); @@ -1501,7 +1504,7 @@ showAuditDialog(FFI ffi) async { maxLength: 256, controller: controller, focusNode: focusNode, - )), + ).workaroundFreezeLinuxMint()), actions: [ dialogButton('Cancel', onPressed: close, isOutline: true), dialogButton('OK', onPressed: submit) @@ -1748,7 +1751,7 @@ void renameDialog( autofocus: true, decoration: InputDecoration(labelText: translate('Name')), validator: validator, - ), + ).workaroundFreezeLinuxMint(), ), ), // NOT use Offstage to wrap LinearProgressIndicator @@ -1808,7 +1811,7 @@ void changeBot({Function()? callback}) async { decoration: InputDecoration( hintText: translate('Token'), ), - ); + ).workaroundFreezeLinuxMint(); return CustomAlertDialog( title: Text(translate("Telegram bot")), @@ -2178,7 +2181,7 @@ void setSharedAbPasswordDialog(String abName, Peer peer) { }, ), ), - ), + ).workaroundFreezeLinuxMint(), if (!gFFI.abModel.current.isPersonal()) Row(children: [ Icon(Icons.info, color: Colors.amber).marginOnly(right: 4), diff --git a/flutter/lib/common/widgets/login.dart b/flutter/lib/common/widgets/login.dart index 71f3dacc3b2c..50e05cde5881 100644 --- a/flutter/lib/common/widgets/login.dart +++ b/flutter/lib/common/widgets/login.dart @@ -678,7 +678,7 @@ Future verificationCodeDialog( labelText: "Email", prefixIcon: Icon(Icons.email)), readOnly: true, controller: TextEditingController(text: user?.email), - )), + ).workaroundFreezeLinuxMint()), isEmailVerification ? const SizedBox(height: 8) : const Offstage(), codeField, /* diff --git a/flutter/lib/common/widgets/my_group.dart b/flutter/lib/common/widgets/my_group.dart index 867d71dff2da..359fbc7f7212 100644 --- a/flutter/lib/common/widgets/my_group.dart +++ b/flutter/lib/common/widgets/my_group.dart @@ -145,7 +145,7 @@ class _MyGroupState extends State { border: InputBorder.none, isDense: true, ), - )), + ).workaroundFreezeLinuxMint()), ], ); } diff --git a/flutter/lib/common/widgets/peer_card.dart b/flutter/lib/common/widgets/peer_card.dart index 0a15eb45b880..b4bca12a9e67 100644 --- a/flutter/lib/common/widgets/peer_card.dart +++ b/flutter/lib/common/widgets/peer_card.dart @@ -1257,7 +1257,7 @@ void _rdpDialog(String id) async { hintText: '3389'), controller: portController, autofocus: true, - ), + ).workaroundFreezeLinuxMint(), ), ], ).marginOnly(bottom: isDesktop ? 8 : 0), @@ -1277,7 +1277,7 @@ void _rdpDialog(String id) async { labelText: isDesktop ? null : translate('Username')), controller: userController, - ), + ).workaroundFreezeLinuxMint(), ), ], ).marginOnly(bottom: stateGlobal.isPortrait.isFalse ? 8 : 0)), @@ -1305,7 +1305,7 @@ void _rdpDialog(String id) async { ? Icons.visibility_off : Icons.visibility))), controller: passwordController, - )), + ).workaroundFreezeLinuxMint()), ), ], )) diff --git a/flutter/lib/common/widgets/peer_tab_page.dart b/flutter/lib/common/widgets/peer_tab_page.dart index 359750788058..9d21ec6cd71d 100644 --- a/flutter/lib/common/widgets/peer_tab_page.dart +++ b/flutter/lib/common/widgets/peer_tab_page.dart @@ -743,7 +743,7 @@ class _PeerSearchBarState extends State { border: InputBorder.none, isDense: true, ), - ), + ).workaroundFreezeLinuxMint(), ), // Icon(Icons.close), IconButton( diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index f2c7121016e1..0ae7affbcf7e 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -424,7 +424,7 @@ class _ConnectionPageState extends State onSubmitted: (_) { onConnect(); }, - )); + ).workaroundFreezeLinuxMint()); }, onSelected: (option) { setState(() { diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index 10f5cc4fdbae..691bed75c9a5 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -237,7 +237,7 @@ class _DesktopHomePageState extends State style: TextStyle( fontSize: 22, ), - ), + ).workaroundFreezeLinuxMint(), ), ) ], @@ -333,7 +333,7 @@ class _DesktopHomePageState extends State EdgeInsets.only(top: 14, bottom: 10), ), style: TextStyle(fontSize: 15), - ), + ).workaroundFreezeLinuxMint(), ), ), if (showOneTime) @@ -940,7 +940,7 @@ void setPasswordDialog({VoidCallback? notEmptyCallback}) async { }); }, maxLength: maxLength, - ), + ).workaroundFreezeLinuxMint(), ), ], ), @@ -967,7 +967,7 @@ void setPasswordDialog({VoidCallback? notEmptyCallback}) async { }); }, maxLength: maxLength, - ), + ).workaroundFreezeLinuxMint(), ), ], ), diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 56a99446c384..d978577ab85c 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -1189,7 +1189,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { contentPadding: EdgeInsets.symmetric(vertical: 12, horizontal: 12), ), - ).marginOnly(right: 15), + ).workaroundFreezeLinuxMint().marginOnly(right: 15), ), Obx(() => ElevatedButton( onPressed: applyEnabled.value && @@ -1346,7 +1346,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { contentPadding: EdgeInsets.symmetric(vertical: 12, horizontal: 12), ), - ).marginOnly(right: 15), + ).workaroundFreezeLinuxMint().marginOnly(right: 15), ), Obx(() => ElevatedButton( onPressed: @@ -2312,7 +2312,7 @@ _LabeledTextField( style: TextStyle( color: disabledTextColor(context, enabled), ), - ), + ).workaroundFreezeLinuxMint(), ], ), ], @@ -2491,7 +2491,7 @@ void changeSocks5Proxy() async { controller: proxyController, autofocus: true, enabled: !isOptFixed, - ), + ).workaroundFreezeLinuxMint(), ), ], ).marginOnly(bottom: 8), @@ -2511,7 +2511,7 @@ void changeSocks5Proxy() async { labelText: isMobile ? translate('Username') : null, ), enabled: !isOptFixed, - ), + ).workaroundFreezeLinuxMint(), ), ], ).marginOnly(bottom: 8), @@ -2537,7 +2537,7 @@ void changeSocks5Proxy() async { controller: pwdController, enabled: !isOptFixed, maxLength: bind.mainMaxEncryptLen(), - )), + ).workaroundFreezeLinuxMint()), ), ], ), diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index 90b8d7dcbf3c..3f555dcaa66e 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -768,7 +768,7 @@ class _FileManagerViewState extends State { ), controller: name, autofocus: true, - ), + ).workaroundFreezeLinuxMint(), ], ), actions: [ @@ -1657,7 +1657,7 @@ class _FileManagerViewState extends State { onChanged: _locationStatus.value == LocationStatus.fileSearchBar ? (searchText) => onSearchText(searchText, isLocal) : null, - ), + ).workaroundFreezeLinuxMint(), ) ], ); diff --git a/flutter/lib/desktop/pages/install_page.dart b/flutter/lib/desktop/pages/install_page.dart index 0ff04240b554..756367c21f1f 100644 --- a/flutter/lib/desktop/pages/install_page.dart +++ b/flutter/lib/desktop/pages/install_page.dart @@ -147,7 +147,7 @@ class _InstallPageBodyState extends State<_InstallPageBody> decoration: InputDecoration( contentPadding: EdgeInsets.all(0.75 * em), ), - ).marginOnly(right: 10), + ).workaroundFreezeLinuxMint().marginOnly(right: 10), ), Obx( () => OutlinedButton.icon( diff --git a/flutter/lib/desktop/pages/port_forward_page.dart b/flutter/lib/desktop/pages/port_forward_page.dart index d6d243c5026a..6671d041bbf5 100644 --- a/flutter/lib/desktop/pages/port_forward_page.dart +++ b/flutter/lib/desktop/pages/port_forward_page.dart @@ -238,7 +238,7 @@ class _PortForwardPageState extends State inputFormatters: inputFormatters, decoration: InputDecoration( hintText: hint, - ))), + )).workaroundFreezeLinuxMint()), ); } diff --git a/flutter/lib/desktop/widgets/remote_toolbar.dart b/flutter/lib/desktop/widgets/remote_toolbar.dart index 839ea1a81db2..d826ea8c6b60 100644 --- a/flutter/lib/desktop/widgets/remote_toolbar.dart +++ b/flutter/lib/desktop/widgets/remote_toolbar.dart @@ -1495,7 +1495,7 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> { ); } - TextField _resolutionInput(TextEditingController controller) { + Widget _resolutionInput(TextEditingController controller) { return TextField( decoration: InputDecoration( border: InputBorder.none, @@ -1509,7 +1509,7 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> { FilteringTextInputFormatter.allow(RegExp(r'[0-9]')), ], controller: controller, - ); + ).workaroundFreezeLinuxMint(); } List _supportedResolutionMenuButtons() => resolutions diff --git a/flutter/lib/mobile/pages/file_manager_page.dart b/flutter/lib/mobile/pages/file_manager_page.dart index e017b5b6faec..b837dc276e39 100644 --- a/flutter/lib/mobile/pages/file_manager_page.dart +++ b/flutter/lib/mobile/pages/file_manager_page.dart @@ -225,7 +225,7 @@ class _FileManagerPageState extends State { errorText: errorText, ), controller: name, - ), + ).workaroundFreezeLinuxMint(), ], ), actions: [ diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index 003640e05e1a..27ce22713803 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -604,7 +604,7 @@ class _RemotePageState extends State with WidgetsBindingObserver { // ko/zh/ja input method: the button will trigger `onKeyEvent` // and the event will not popup if `KeyEventResult.handled` is returned. onChanged: handleSoftKeyboardInput, - ), + ).workaroundFreezeLinuxMint(), ), ]; if (showCursorPaint) { diff --git a/flutter/lib/mobile/widgets/dialog.dart b/flutter/lib/mobile/widgets/dialog.dart index 1c8b4dd3d7b8..ebedd79d44fe 100644 --- a/flutter/lib/mobile/widgets/dialog.dart +++ b/flutter/lib/mobile/widgets/dialog.dart @@ -66,7 +66,7 @@ void setPermanentPasswordDialog(OverlayDialogManager dialogManager) async { ? null : translate('Too short, at least 6 characters.'); }, - ), + ).workaroundFreezeLinuxMint(), TextFormField( obscureText: true, keyboardType: TextInputType.visiblePassword, @@ -85,7 +85,7 @@ void setPermanentPasswordDialog(OverlayDialogManager dialogManager) async { ? null : translate('The confirmation is not identical.'); }, - ), + ).workaroundFreezeLinuxMint(), ])), onCancel: close, onSubmit: (validateLength && validateSame) ? submit : null, @@ -216,7 +216,7 @@ void showServerSettingsWithValue( ), validator: validator, autofocus: autofocus, - ), + ).workaroundFreezeLinuxMint(), ), ], ); @@ -229,7 +229,7 @@ void showServerSettingsWithValue( errorText: errorMsg.isEmpty ? null : errorMsg, ), validator: validator, - ); + ).workaroundFreezeLinuxMint(); } return CustomAlertDialog( From 1f5aeda41dcd3c1844a3a8328d7221076cbac2db Mon Sep 17 00:00:00 2001 From: jkh0kr Date: Fri, 20 Dec 2024 16:09:33 +0900 Subject: [PATCH 489/541] Update ko.rs (#10320) --- src/lang/ko.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lang/ko.rs b/src/lang/ko.rs index fa35507f8491..6a2815aceac7 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -655,6 +655,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is synchronized", "클립보드가 동기화됨"), ("Update client clipboard", "클라이언트 클립보드 업데이트"), ("Untagged", "태그 없음"), - ("new-version-of-{}-tip", ""), + ("new-version-of-{}-tip", "{} 의 새로운 버전이 출시되었습니다."), ].iter().cloned().collect(); } From 25e438a6631011dfcec7ee6c15208b4d6ac914e4 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Fri, 20 Dec 2024 22:24:53 +0800 Subject: [PATCH 490/541] crate --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 5949b5925856..dae69ead3a1d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -83,7 +83,7 @@ ringbuf = "0.3" [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] mac_address = "1.1" -sciter-rs = { git = "https://github.com/open-trade/rust-sciter", branch = "dyn" } +sciter-rs = { git = "https://github.com/rustdesk-org/rust-sciter", branch = "dyn" } sys-locale = "0.3" enigo = { path = "libs/enigo", features = [ "with_serde" ] } clipboard = { path = "libs/clipboard" } @@ -149,7 +149,7 @@ reqwest = { git = "https://github.com/rustdesk-org/reqwest", features = ["blocki [target.'cfg(target_os = "linux")'.dependencies] psimple = { package = "libpulse-simple-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/rustdesk-org/pulsectl" } async-process = "1.7" evdev = { git="https://github.com/rustdesk-org/evdev" } dbus = "0.9" From bc461fe99b1838897f39134d1a0eb01abdb92b52 Mon Sep 17 00:00:00 2001 From: 21pages Date: Fri, 20 Dec 2024 22:46:42 +0800 Subject: [PATCH 491/541] Revert "Revert "revert linux use cpal "" (#10326) * Revert "Revert "revert linux use cpal (#10260)" (#10262)" This reverts commit 827b5f6a4c0c0208431291177c3d42b271dad835. * update Cargo.lock Signed-off-by: 21pages --------- Signed-off-by: 21pages --- Cargo.lock | 4 ++-- Cargo.toml | 3 +++ src/client.rs | 57 +++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 62 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3fc53573b4e1..5dfddf127855 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5466,7 +5466,7 @@ dependencies = [ [[package]] name = "rust-pulsectl" version = "0.2.12" -source = "git+https://github.com/open-trade/pulsectl#5e68f4c2b7c644fa321984688602d71e8ad0bba3" +source = "git+https://github.com/rustdesk-org/pulsectl#aa34dde499aa912a3abc5289cc0b547bd07dd6e2" dependencies = [ "libpulse-binding", ] @@ -5813,7 +5813,7 @@ dependencies = [ [[package]] name = "sciter-rs" version = "0.5.57" -source = "git+https://github.com/open-trade/rust-sciter?branch=dyn#5322f3a755a0e6bf999fbc60d1efc35246c0f821" +source = "git+https://github.com/rustdesk-org/rust-sciter?branch=dyn#5322f3a755a0e6bf999fbc60d1efc35246c0f821" dependencies = [ "lazy_static", "libc", diff --git a/Cargo.toml b/Cargo.toml index dae69ead3a1d..f2f3076bef76 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -78,6 +78,9 @@ fon = "0.6" zip = "0.6" shutdown_hooks = "0.1" totp-rs = { version = "5.4", default-features = false, features = ["gen_secret", "otpauth"] } + +[target.'cfg(not(target_os = "linux"))'.dependencies] +# https://github.com/rustdesk/rustdesk/discussions/10197, not use cpal on linux cpal = { git = "https://github.com/rustdesk-org/cpal", branch = "osx-screencapturekit" } ringbuf = "0.3" diff --git a/src/client.rs b/src/client.rs index 83b6de43a376..05161438325f 100644 --- a/src/client.rs +++ b/src/client.rs @@ -2,12 +2,14 @@ use async_trait::async_trait; use bytes::Bytes; #[cfg(not(any(target_os = "android", target_os = "ios")))] use clipboard_master::{CallbackResult, ClipboardHandler}; +#[cfg(not(target_os = "linux"))] use cpal::{ traits::{DeviceTrait, HostTrait, StreamTrait}, Device, Host, StreamConfig, }; use crossbeam_queue::ArrayQueue; use magnum_opus::{Channels::*, Decoder as AudioDecoder}; +#[cfg(not(target_os = "linux"))] use ringbuf::{ring_buffer::RbBase, Rb}; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; @@ -117,6 +119,7 @@ pub const SCRAP_OTHER_VERSION_OR_X11_REQUIRED: &str = pub const SCRAP_X11_REQUIRED: &str = "x11 expected"; pub const SCRAP_X11_REF_URL: &str = "https://rustdesk.com/docs/en/manual/linux/#x11-required"; +#[cfg(not(target_os = "linux"))] pub const AUDIO_BUFFER_MS: usize = 3000; #[cfg(feature = "flutter")] @@ -139,6 +142,7 @@ struct TextClipboardState { running: bool, } +#[cfg(not(target_os = "linux"))] lazy_static::lazy_static! { static ref AUDIO_HOST: Host = cpal::default_host(); } @@ -861,20 +865,28 @@ impl ClipboardHandler for ClientClipboardHandler { #[derive(Default)] pub struct AudioHandler { audio_decoder: Option<(AudioDecoder, Vec)>, + #[cfg(target_os = "linux")] + simple: Option, + #[cfg(not(target_os = "linux"))] audio_buffer: AudioBuffer, sample_rate: (u32, u32), + #[cfg(not(target_os = "linux"))] audio_stream: Option>, channels: u16, + #[cfg(not(target_os = "linux"))] device_channel: u16, + #[cfg(not(target_os = "linux"))] ready: Arc>, } +#[cfg(not(target_os = "linux"))] struct AudioBuffer( pub Arc>>, usize, [usize; 30], ); +#[cfg(not(target_os = "linux"))] impl Default for AudioBuffer { fn default() -> Self { Self( @@ -887,6 +899,7 @@ impl Default for AudioBuffer { } } +#[cfg(not(target_os = "linux"))] impl AudioBuffer { pub fn resize(&mut self, sample_rate: usize, channels: usize) { let capacity = sample_rate * channels * AUDIO_BUFFER_MS / 1000; @@ -989,7 +1002,37 @@ impl AudioBuffer { } impl AudioHandler { + #[cfg(target_os = "linux")] + fn start_audio(&mut self, format0: AudioFormat) -> ResultType<()> { + use psimple::Simple; + use pulse::sample::{Format, Spec}; + use pulse::stream::Direction; + + let spec = Spec { + format: Format::F32le, + channels: format0.channels as _, + rate: format0.sample_rate as _, + }; + if !spec.is_valid() { + bail!("Invalid audio format"); + } + + self.simple = Some(Simple::new( + None, // Use the default server + &crate::get_app_name(), // Our application’s name + Direction::Playback, // We want a playback stream + None, // Use the default device + "playback", // Description of our stream + &spec, // Our sample format + None, // Use default channel map + None, // Use default buffering attributes + )?); + self.sample_rate = (format0.sample_rate, format0.sample_rate); + Ok(()) + } + /// Start the audio playback. + #[cfg(not(target_os = "linux"))] fn start_audio(&mut self, format0: AudioFormat) -> ResultType<()> { let device = AUDIO_HOST .default_output_device() @@ -1057,13 +1100,20 @@ impl AudioHandler { /// Handle audio frame and play it. #[inline] pub fn handle_frame(&mut self, frame: AudioFrame) { + #[cfg(not(target_os = "linux"))] if self.audio_stream.is_none() || !self.ready.lock().unwrap().clone() { return; } + #[cfg(target_os = "linux")] + if self.simple.is_none() { + log::debug!("PulseAudio simple binding does not exists"); + return; + } self.audio_decoder.as_mut().map(|(d, buffer)| { if let Ok(n) = d.decode_float(&frame.data, buffer, false) { let channels = self.channels; let n = n * (channels as usize); + #[cfg(not(target_os = "linux"))] { let sample_rate0 = self.sample_rate.0; let sample_rate = self.sample_rate.1; @@ -1087,11 +1137,18 @@ impl AudioHandler { } self.audio_buffer.append_pcm(&buffer); } + #[cfg(target_os = "linux")] + { + let data_u8 = + unsafe { std::slice::from_raw_parts::(buffer.as_ptr() as _, n * 4) }; + self.simple.as_mut().map(|x| x.write(data_u8)); + } } }); } /// Build audio output stream for current device. + #[cfg(not(target_os = "linux"))] fn build_output_stream>( &mut self, config: &StreamConfig, From b24551da7b07b6a830c4237228c3f74dbb7682fd Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Sat, 21 Dec 2024 14:53:28 +0800 Subject: [PATCH 492/541] refact: linux, move rustdesk into /usr/share (#10327) * refact: linux, move rustdesk into /usr/share Signed-off-by: fufesou * linux, upgrade, try remove old empty folders Signed-off-by: fufesou --------- Signed-off-by: fufesou --- .github/workflows/flutter-build.yml | 2 +- appimage/AppImageBuilder-aarch64.yml | 4 ++-- appimage/AppImageBuilder-x86_64.yml | 4 ++-- build.py | 16 ++++++++-------- flatpak/rustdesk.json | 2 +- res/DEBIAN/postinst | 2 +- res/PKGBUILD | 4 ++-- res/rpm-flutter-suse.spec | 12 ++++++++---- res/rpm-flutter.spec | 12 ++++++++---- res/rpm-suse.spec | 9 +++++---- res/rpm.spec | 9 +++++---- src/ui.rs | 4 ++-- 12 files changed, 45 insertions(+), 35 deletions(-) diff --git a/.github/workflows/flutter-build.yml b/.github/workflows/flutter-build.yml index f9e633348c8d..73e1efe38781 100644 --- a/.github/workflows/flutter-build.yml +++ b/.github/workflows/flutter-build.yml @@ -719,7 +719,7 @@ jobs: shell: bash run: | cd "$(dirname "$(which flutter)")" - # https://github.com/flutter/flutter/issues/1.3.53 + # https://github.com/flutter/flutter/issues/133533 sed -i -e 's/_setFramesEnabledState(false);/\/\/_setFramesEnabledState(false);/g' ../packages/flutter/lib/src/scheduler/binding.dart grep -n '_setFramesEnabledState(false);' ../packages/flutter/lib/src/scheduler/binding.dart diff --git a/appimage/AppImageBuilder-aarch64.yml b/appimage/AppImageBuilder-aarch64.yml index 9f8490ac99e1..64792efe9a5d 100644 --- a/appimage/AppImageBuilder-aarch64.yml +++ b/appimage/AppImageBuilder-aarch64.yml @@ -19,7 +19,7 @@ AppDir: name: rustdesk icon: rustdesk version: 1.3.5 - exec: usr/local/rustdesk/rustdesk + exec: usr/share/rustdesk/rustdesk exec_args: $@ apt: arch: @@ -77,7 +77,7 @@ AppDir: env: GIO_MODULE_DIR: /lib64/gio/modules:/usr/lib/aarch64-linux-gnu/gio/modules:$APPDIR/usr/lib/aarch64-linux-gnu/gio/modules 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/local/rustdesk/lib:$APPDIR/lib/aarch64 + APPDIR_LIBRARY_PATH: /lib64:/usr/lib/aarch64-linux-gnu:$APPDIR/lib/aarch64-linux-gnu:$APPDIR/lib/aarch64-linux-gnu/security:$APPDIR/lib/systemd:$APPDIR/usr/lib/aarch64-linux-gnu:$APPDIR/usr/lib/aarch64-linux-gnu/gdk-pixbuf-2.0/2.10.0/loaders:$APPDIR/usr/lib/aarch64-linux-gnu/gstreamer-1.0:$APPDIR/usr/lib/aarch64-linux-gnu/gtk-3.0/3.0.0/immodules:$APPDIR/usr/lib/aarch64-linux-gnu/gtk-3.0/3.0.0/printbackends:$APPDIR/usr/lib/aarch64-linux-gnu/krb5/plugins/preauth:$APPDIR/usr/lib/aarch64-linux-gnu/libcanberra-0.30:$APPDIR/usr/lib/aarch64-linux-gnu/pulseaudio:$APPDIR/usr/lib/aarch64-linux-gnu/sasl2:$APPDIR/usr/lib/aarch64-linux-gnu/vdpau:$APPDIR/usr/share/rustdesk/lib:$APPDIR/lib/aarch64 GST_PLUGIN_PATH: /lib64/gstreamer-1.0:/usr/lib/aarch64-linux-gnu/gstreamer-1.0:$APPDIR/usr/lib/aarch64-linux-gnu/gstreamer-1.0 GST_PLUGIN_SYSTEM_PATH: /lib64/gstreamer-1.0:/usr/lib/aarch64-linux-gnu/gstreamer-1.0:$APPDIR/usr/lib/aarch64-linux-gnu/gstreamer-1.0 test: diff --git a/appimage/AppImageBuilder-x86_64.yml b/appimage/AppImageBuilder-x86_64.yml index 8a5f942341c3..ea0db9d52cf6 100644 --- a/appimage/AppImageBuilder-x86_64.yml +++ b/appimage/AppImageBuilder-x86_64.yml @@ -19,7 +19,7 @@ AppDir: name: rustdesk icon: rustdesk version: 1.3.5 - exec: usr/local/rustdesk/rustdesk + exec: usr/share/rustdesk/rustdesk exec_args: $@ apt: arch: @@ -80,7 +80,7 @@ AppDir: env: 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 - 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/local/rustdesk/lib:$APPDIR/lib/x86_64 + APPDIR_LIBRARY_PATH: /lib64:/usr/lib/x86_64-linux-gnu:$APPDIR/lib/x86_64-linux-gnu:$APPDIR/lib/x86_64-linux-gnu/security:$APPDIR/lib/systemd:$APPDIR/usr/lib/x86_64-linux-gnu:$APPDIR/usr/lib/x86_64-linux-gnu/gdk-pixbuf-2.0/2.10.0/loaders:$APPDIR/usr/lib/x86_64-linux-gnu/gstreamer-1.0:$APPDIR/usr/lib/x86_64-linux-gnu/gtk-3.0/3.0.0/immodules:$APPDIR/usr/lib/x86_64-linux-gnu/gtk-3.0/3.0.0/printbackends:$APPDIR/usr/lib/x86_64-linux-gnu/krb5/plugins/preauth:$APPDIR/usr/lib/x86_64-linux-gnu/libcanberra-0.30:$APPDIR/usr/lib/x86_64-linux-gnu/pulseaudio:$APPDIR/usr/lib/x86_64-linux-gnu/sasl2:$APPDIR/usr/lib/x86_64-linux-gnu/vdpau:$APPDIR/usr/share/rustdesk/lib:$APPDIR/lib/x86_64 GST_PLUGIN_PATH: /lib64/gstreamer-1.0:/usr/lib/x86_64-linux-gnu/gstreamer-1.0:$APPDIR/usr/lib/x86_64-linux-gnu/gstreamer-1.0 GST_PLUGIN_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: diff --git a/build.py b/build.py index 802d6a7a69f6..f37548481975 100755 --- a/build.py +++ b/build.py @@ -321,7 +321,7 @@ def build_flutter_deb(version, features): os.chdir('flutter') system2('flutter build linux --release') system2('mkdir -p tmpdeb/usr/bin/') - system2('mkdir -p tmpdeb/usr/local/rustdesk') + system2('mkdir -p tmpdeb/usr/share/rustdesk') system2('mkdir -p tmpdeb/etc/rustdesk/') system2('mkdir -p tmpdeb/etc/pam.d/') system2('mkdir -p tmpdeb/usr/share/rustdesk/files/systemd/') @@ -331,7 +331,7 @@ def build_flutter_deb(version, features): system2('mkdir -p tmpdeb/usr/share/polkit-1/actions') system2('rm tmpdeb/usr/bin/rustdesk || true') system2( - f'cp -r {flutter_build_dir}/* tmpdeb/usr/local/rustdesk/') + f'cp -r {flutter_build_dir}/* tmpdeb/usr/share/rustdesk/') system2( 'cp ../res/rustdesk.service tmpdeb/usr/share/rustdesk/files/systemd/') system2( @@ -366,7 +366,7 @@ def build_flutter_deb(version, features): def build_deb_from_folder(version, binary_folder): os.chdir('flutter') system2('mkdir -p tmpdeb/usr/bin/') - system2('mkdir -p tmpdeb/usr/local/rustdesk') + system2('mkdir -p tmpdeb/usr/share/rustdesk') system2('mkdir -p tmpdeb/usr/share/rustdesk/files/systemd/') system2('mkdir -p tmpdeb/usr/share/icons/hicolor/256x256/apps/') system2('mkdir -p tmpdeb/usr/share/icons/hicolor/scalable/apps/') @@ -374,7 +374,7 @@ def build_deb_from_folder(version, binary_folder): system2('mkdir -p tmpdeb/usr/share/polkit-1/actions') system2('rm tmpdeb/usr/bin/rustdesk || true') system2( - f'cp -r ../{binary_folder}/* tmpdeb/usr/local/rustdesk/') + f'cp -r ../{binary_folder}/* tmpdeb/usr/share/rustdesk/') system2( 'cp ../res/rustdesk.service tmpdeb/usr/share/rustdesk/files/systemd/') system2( @@ -621,14 +621,14 @@ def main(): os.system('mkdir -p tmpdeb/etc/pam.d/') os.system('cp pam.d/rustdesk.debian tmpdeb/etc/pam.d/rustdesk') system2('strip tmpdeb/usr/bin/rustdesk') - system2('mkdir -p tmpdeb/usr/local/rustdesk') - system2('mv tmpdeb/usr/bin/rustdesk tmpdeb/usr/local/rustdesk/') - system2('cp libsciter-gtk.so tmpdeb/usr/local/rustdesk/') + system2('mkdir -p tmpdeb/usr/share/rustdesk') + system2('mv tmpdeb/usr/bin/rustdesk tmpdeb/usr/share/rustdesk/') + system2('cp libsciter-gtk.so tmpdeb/usr/share/rustdesk/') md5_file('usr/share/rustdesk/files/systemd/rustdesk.service') md5_file('etc/rustdesk/startwm.sh') md5_file('etc/X11/rustdesk/xorg.conf') md5_file('etc/pam.d/rustdesk') - md5_file('usr/local/rustdesk/libsciter-gtk.so') + md5_file('usr/share/rustdesk/libsciter-gtk.so') system2('dpkg-deb -b tmpdeb rustdesk.deb; /bin/rm -rf tmpdeb/') os.rename('rustdesk.deb', 'rustdesk-%s.deb' % version) diff --git a/flatpak/rustdesk.json b/flatpak/rustdesk.json index 2aab79aea62d..af1bc5fe74a1 100644 --- a/flatpak/rustdesk.json +++ b/flatpak/rustdesk.json @@ -39,7 +39,7 @@ "build-commands": [ "bsdtar -Oxf rustdesk.deb data.tar.xz | bsdtar -xf -", "cp -r usr/* /app/", - "mkdir -p /app/bin && ln -s /app/local/rustdesk/rustdesk /app/bin/rustdesk" + "mkdir -p /app/bin && ln -s /app/share/rustdesk/rustdesk /app/bin/rustdesk" ], "sources": [ { diff --git a/res/DEBIAN/postinst b/res/DEBIAN/postinst index 07956f11fa34..5f642daac454 100755 --- a/res/DEBIAN/postinst +++ b/res/DEBIAN/postinst @@ -5,7 +5,7 @@ set -e if [ "$1" = configure ]; then INITSYS=$(ls -al /proc/1/exe | awk -F' ' '{print $NF}' | awk -F'/' '{print $NF}') - ln -s /usr/local/rustdesk/rustdesk /usr/bin/rustdesk + ln -s /usr/share/rustdesk/rustdesk /usr/bin/rustdesk if [ "systemd" == "$INITSYS" ]; then diff --git a/res/PKGBUILD b/res/PKGBUILD index 49a5a4cadf98..7154ad5abe1f 100644 --- a/res/PKGBUILD +++ b/res/PKGBUILD @@ -23,10 +23,10 @@ md5sums=() #generate with 'makepkg -g' package() { if [[ ${FLUTTER} ]]; then - mkdir -p "${pkgdir}/usr/local/rustdesk" && cp -r ${HBB}/flutter/build/linux/x64/release/bundle/* -t "${pkgdir}/usr/local/rustdesk" + mkdir -p "${pkgdir}/usr/share/rustdesk" && cp -r ${HBB}/flutter/build/linux/x64/release/bundle/* -t "${pkgdir}/usr/share/rustdesk" fi mkdir -p "${pkgdir}/usr/bin" - pushd ${pkgdir} && ln -s /usr/local/rustdesk/rustdesk usr/bin/rustdesk && popd + pushd ${pkgdir} && ln -s /usr/share/rustdesk/rustdesk usr/bin/rustdesk && popd install -Dm 644 $HBB/res/rustdesk.service -t "${pkgdir}/usr/share/rustdesk/files" install -Dm 644 $HBB/res/rustdesk.desktop -t "${pkgdir}/usr/share/rustdesk/files" install -Dm 644 $HBB/res/rustdesk-link.desktop -t "${pkgdir}/usr/share/rustdesk/files" diff --git a/res/rpm-flutter-suse.spec b/res/rpm-flutter-suse.spec index 7d0178be2fd7..63e38acc8608 100644 --- a/res/rpm-flutter-suse.spec +++ b/res/rpm-flutter-suse.spec @@ -9,6 +9,8 @@ Requires: gtk3 libxcb1 xdotool libXfixes3 alsa-utils libXtst6 libva2 pam gstre Recommends: libayatana-appindicator3-1 Provides: libdesktop_drop_plugin.so()(64bit), libdesktop_multi_window_plugin.so()(64bit), libfile_selector_linux_plugin.so()(64bit), libflutter_custom_cursor_plugin.so()(64bit), libflutter_linux_gtk.so()(64bit), libscreen_retriever_plugin.so()(64bit), libtray_manager_plugin.so()(64bit), liburl_launcher_linux_plugin.so()(64bit), libwindow_manager_plugin.so()(64bit), libwindow_size_plugin.so()(64bit), libtexture_rgba_renderer_plugin.so()(64bit) +# https://docs.fedoraproject.org/en-US/packaging-guidelines/Scriptlets/ + %description The best open-source remote desktop client software, written in Rust. @@ -22,7 +24,7 @@ The best open-source remote desktop client software, written in Rust. %install -mkdir -p "%{buildroot}/usr/local/rustdesk" && cp -r ${HBB}/flutter/build/linux/x64/release/bundle/* -t "%{buildroot}/usr/local/rustdesk" +mkdir -p "%{buildroot}/usr/share/rustdesk" && cp -r ${HBB}/flutter/build/linux/x64/release/bundle/* -t "%{buildroot}/usr/share/rustdesk" mkdir -p "%{buildroot}/usr/bin" install -Dm 644 $HBB/res/rustdesk.service -t "%{buildroot}/usr/share/rustdesk/files" install -Dm 644 $HBB/res/rustdesk.desktop -t "%{buildroot}/usr/share/rustdesk/files" @@ -31,7 +33,7 @@ install -Dm 644 $HBB/res/128x128@2x.png "%{buildroot}/usr/share/icons/hicolor/25 install -Dm 644 $HBB/res/scalable.svg "%{buildroot}/usr/share/icons/hicolor/scalable/apps/rustdesk.svg" %files -/usr/local/rustdesk/* +/usr/share/rustdesk/* /usr/share/rustdesk/files/rustdesk.service /usr/share/icons/hicolor/256x256/apps/rustdesk.png /usr/share/icons/hicolor/scalable/apps/rustdesk.svg @@ -41,7 +43,6 @@ install -Dm 644 $HBB/res/scalable.svg "%{buildroot}/usr/share/icons/hicolor/scal %changelog # let's skip this for now -# https://www.cnblogs.com/xingmuxin/p/8990255.html %pre # can do something for centos7 case "$1" in @@ -58,7 +59,7 @@ esac cp /usr/share/rustdesk/files/rustdesk.service /etc/systemd/system/rustdesk.service cp /usr/share/rustdesk/files/rustdesk.desktop /usr/share/applications/ cp /usr/share/rustdesk/files/rustdesk-link.desktop /usr/share/applications/ -ln -sf /usr/local/rustdesk/rustdesk /usr/bin/rustdesk +ln -sf /usr/share/rustdesk/rustdesk /usr/bin/rustdesk systemctl daemon-reload systemctl enable rustdesk systemctl start rustdesk @@ -84,11 +85,14 @@ case "$1" in rm /usr/bin/rustdesk || true rmdir /usr/lib/rustdesk || true rmdir /usr/local/rustdesk || true + rmdir /usr/share/rustdesk || true rm /usr/share/applications/rustdesk.desktop || true rm /usr/share/applications/rustdesk-link.desktop || true update-desktop-database ;; 1) # for upgrade + rmdir /usr/lib/rustdesk || true + rmdir /usr/local/rustdesk || true ;; esac diff --git a/res/rpm-flutter.spec b/res/rpm-flutter.spec index 24fd2ab841ee..1359cb4100cc 100644 --- a/res/rpm-flutter.spec +++ b/res/rpm-flutter.spec @@ -9,6 +9,8 @@ Requires: gtk3 libxcb libxdo libXfixes alsa-lib libva pam gstreamer1-plugins-b Recommends: libayatana-appindicator-gtk3 Provides: libdesktop_drop_plugin.so()(64bit), libdesktop_multi_window_plugin.so()(64bit), libfile_selector_linux_plugin.so()(64bit), libflutter_custom_cursor_plugin.so()(64bit), libflutter_linux_gtk.so()(64bit), libscreen_retriever_plugin.so()(64bit), libtray_manager_plugin.so()(64bit), liburl_launcher_linux_plugin.so()(64bit), libwindow_manager_plugin.so()(64bit), libwindow_size_plugin.so()(64bit), libtexture_rgba_renderer_plugin.so()(64bit) +# https://docs.fedoraproject.org/en-US/packaging-guidelines/Scriptlets/ + %description The best open-source remote desktop client software, written in Rust. @@ -22,7 +24,7 @@ The best open-source remote desktop client software, written in Rust. %install -mkdir -p "%{buildroot}/usr/local/rustdesk" && cp -r ${HBB}/flutter/build/linux/x64/release/bundle/* -t "%{buildroot}/usr/local/rustdesk" +mkdir -p "%{buildroot}/usr/share/rustdesk" && cp -r ${HBB}/flutter/build/linux/x64/release/bundle/* -t "%{buildroot}/usr/share/rustdesk" mkdir -p "%{buildroot}/usr/bin" install -Dm 644 $HBB/res/rustdesk.service -t "%{buildroot}/usr/share/rustdesk/files" install -Dm 644 $HBB/res/rustdesk.desktop -t "%{buildroot}/usr/share/rustdesk/files" @@ -31,7 +33,7 @@ install -Dm 644 $HBB/res/128x128@2x.png "%{buildroot}/usr/share/icons/hicolor/25 install -Dm 644 $HBB/res/scalable.svg "%{buildroot}/usr/share/icons/hicolor/scalable/apps/rustdesk.svg" %files -/usr/local/rustdesk/* +/usr/share/rustdesk/* /usr/share/rustdesk/files/rustdesk.service /usr/share/icons/hicolor/256x256/apps/rustdesk.png /usr/share/icons/hicolor/scalable/apps/rustdesk.svg @@ -41,7 +43,6 @@ install -Dm 644 $HBB/res/scalable.svg "%{buildroot}/usr/share/icons/hicolor/scal %changelog # let's skip this for now -# https://www.cnblogs.com/xingmuxin/p/8990255.html %pre # can do something for centos7 case "$1" in @@ -58,7 +59,7 @@ esac cp /usr/share/rustdesk/files/rustdesk.service /etc/systemd/system/rustdesk.service cp /usr/share/rustdesk/files/rustdesk.desktop /usr/share/applications/ cp /usr/share/rustdesk/files/rustdesk-link.desktop /usr/share/applications/ -ln -sf /usr/local/rustdesk/rustdesk /usr/bin/rustdesk +ln -sf /usr/share/rustdesk/rustdesk /usr/bin/rustdesk systemctl daemon-reload systemctl enable rustdesk systemctl start rustdesk @@ -84,11 +85,14 @@ case "$1" in rm /usr/bin/rustdesk || true rmdir /usr/lib/rustdesk || true rmdir /usr/local/rustdesk || true + rmdir /usr/share/rustdesk || true rm /usr/share/applications/rustdesk.desktop || true rm /usr/share/applications/rustdesk-link.desktop || true update-desktop-database ;; 1) # for upgrade + rmdir /usr/lib/rustdesk || true + rmdir /usr/local/rustdesk || true ;; esac diff --git a/res/rpm-suse.spec b/res/rpm-suse.spec index 5686b74ef473..79b26d6f07c9 100644 --- a/res/rpm-suse.spec +++ b/res/rpm-suse.spec @@ -6,6 +6,8 @@ License: GPL-3.0 Requires: gtk3 libxcb1 xdotool libXfixes3 alsa-utils libXtst6 libva2 pam gstreamer-plugins-base gstreamer-plugin-pipewire Recommends: libayatana-appindicator3-1 +# https://docs.fedoraproject.org/en-US/packaging-guidelines/Scriptlets/ + %description The best open-source remote desktop client software, written in Rust. @@ -19,12 +21,12 @@ The best open-source remote desktop client software, written in Rust. %install mkdir -p %{buildroot}/usr/bin/ -mkdir -p %{buildroot}/usr/local/rustdesk/ +mkdir -p %{buildroot}/usr/share/rustdesk/ mkdir -p %{buildroot}/usr/share/rustdesk/files/ mkdir -p %{buildroot}/usr/share/icons/hicolor/256x256/apps/ mkdir -p %{buildroot}/usr/share/icons/hicolor/scalable/apps/ install -m 755 $HBB/target/release/rustdesk %{buildroot}/usr/bin/rustdesk -install $HBB/libsciter-gtk.so %{buildroot}/usr/local/rustdesk/libsciter-gtk.so +install $HBB/libsciter-gtk.so %{buildroot}/usr/share/rustdesk/libsciter-gtk.so install $HBB/res/rustdesk.service %{buildroot}/usr/share/rustdesk/files/ install $HBB/res/128x128@2x.png %{buildroot}/usr/share/icons/hicolor/256x256/apps/rustdesk.png install $HBB/res/scalable.svg %{buildroot}/usr/share/icons/hicolor/scalable/apps/rustdesk.svg @@ -33,7 +35,7 @@ install $HBB/res/rustdesk-link.desktop %{buildroot}/usr/share/rustdesk/files/ %files /usr/bin/rustdesk -/usr/local/rustdesk/libsciter-gtk.so +/usr/share/rustdesk/libsciter-gtk.so /usr/share/rustdesk/files/rustdesk.service /usr/share/icons/hicolor/256x256/apps/rustdesk.png /usr/share/icons/hicolor/scalable/apps/rustdesk.svg @@ -43,7 +45,6 @@ install $HBB/res/rustdesk-link.desktop %{buildroot}/usr/share/rustdesk/files/ %changelog # let's skip this for now -# https://www.cnblogs.com/xingmuxin/p/8990255.html %pre # can do something for centos7 case "$1" in diff --git a/res/rpm.spec b/res/rpm.spec index 874f83aa87a4..860d05df2a1b 100644 --- a/res/rpm.spec +++ b/res/rpm.spec @@ -8,6 +8,8 @@ Vendor: rustdesk Requires: gtk3 libxcb libxdo libXfixes alsa-lib libva2 pam gstreamer1-plugins-base Recommends: libayatana-appindicator-gtk3 +# https://docs.fedoraproject.org/en-US/packaging-guidelines/Scriptlets/ + %description The best open-source remote desktop client software, written in Rust. @@ -21,12 +23,12 @@ The best open-source remote desktop client software, written in Rust. %install mkdir -p %{buildroot}/usr/bin/ -mkdir -p %{buildroot}/usr/local/rustdesk/ +mkdir -p %{buildroot}/usr/share/rustdesk/ mkdir -p %{buildroot}/usr/share/rustdesk/files/ mkdir -p %{buildroot}/usr/share/icons/hicolor/256x256/apps/ mkdir -p %{buildroot}/usr/share/icons/hicolor/scalable/apps/ install -m 755 $HBB/target/release/rustdesk %{buildroot}/usr/bin/rustdesk -install $HBB/libsciter-gtk.so %{buildroot}/usr/local/rustdesk/libsciter-gtk.so +install $HBB/libsciter-gtk.so %{buildroot}/usr/share/rustdesk/libsciter-gtk.so install $HBB/res/rustdesk.service %{buildroot}/usr/share/rustdesk/files/ install $HBB/res/128x128@2x.png %{buildroot}/usr/share/icons/hicolor/256x256/apps/rustdesk.png install $HBB/res/scalable.svg %{buildroot}/usr/share/icons/hicolor/scalable/apps/rustdesk.svg @@ -35,7 +37,7 @@ install $HBB/res/rustdesk-link.desktop %{buildroot}/usr/share/rustdesk/files/ %files /usr/bin/rustdesk -/usr/local/rustdesk/libsciter-gtk.so +/usr/share/rustdesk/libsciter-gtk.so /usr/share/rustdesk/files/rustdesk.service /usr/share/icons/hicolor/256x256/apps/rustdesk.png /usr/share/icons/hicolor/scalable/apps/rustdesk.svg @@ -46,7 +48,6 @@ install $HBB/res/rustdesk-link.desktop %{buildroot}/usr/share/rustdesk/files/ %changelog # let's skip this for now -# https://www.cnblogs.com/xingmuxin/p/8990255.html %pre # can do something for centos7 case "$1" in diff --git a/src/ui.rs b/src/ui.rs index 79a2207e63b2..27586a54fce5 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -42,7 +42,7 @@ pub fn start(args: &mut [String]) { #[cfg(all(target_os = "linux", feature = "inline"))] { let app_dir = std::env::var("APPDIR").unwrap_or("".to_string()); - let mut so_path = "/usr/local/rustdesk/libsciter-gtk.so".to_owned(); + let mut so_path = "/usr/share/rustdesk/libsciter-gtk.so".to_owned(); for (prefix, dir) in [ ("", "/usr"), ("", "/app"), @@ -51,7 +51,7 @@ pub fn start(args: &mut [String]) { ] .iter() { - let path = format!("{prefix}{dir}/local/rustdesk/libsciter-gtk.so"); + let path = format!("{prefix}{dir}/share/rustdesk/libsciter-gtk.so"); if std::path::Path::new(&path).exists() { so_path = path; break; From 03999d900e325d81c7bb11da1ad60bf43c31a8cf Mon Sep 17 00:00:00 2001 From: rustdesk Date: Sat, 21 Dec 2024 15:00:16 +0800 Subject: [PATCH 493/541] 1.3.6 --- .github/workflows/flutter-build.yml | 2 +- .github/workflows/playground.yml | 2 +- Cargo.lock | 4 ++-- Cargo.toml | 2 +- appimage/AppImageBuilder-aarch64.yml | 2 +- appimage/AppImageBuilder-x86_64.yml | 2 +- flutter/pubspec.yaml | 2 +- libs/portable/Cargo.toml | 2 +- res/PKGBUILD | 2 +- res/rpm-flutter-suse.spec | 2 +- res/rpm-flutter.spec | 2 +- res/rpm.spec | 2 +- 12 files changed, 13 insertions(+), 13 deletions(-) diff --git a/.github/workflows/flutter-build.yml b/.github/workflows/flutter-build.yml index 73e1efe38781..ea525f525321 100644 --- a/.github/workflows/flutter-build.yml +++ b/.github/workflows/flutter-build.yml @@ -33,7 +33,7 @@ env: VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite" # vcpkg version: 2024.11.16 VCPKG_COMMIT_ID: "b2cb0da531c2f1f740045bfe7c4dac59f0b2b69c" - VERSION: "1.3.5" + VERSION: "1.3.6" NDK_VERSION: "r27c" #signing keys env variable checks ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}" diff --git a/.github/workflows/playground.yml b/.github/workflows/playground.yml index bf7dcd19ecff..46a6dd0c30f5 100644 --- a/.github/workflows/playground.yml +++ b/.github/workflows/playground.yml @@ -18,7 +18,7 @@ env: VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite" # vcpkg version: 2024.11.16 VCPKG_COMMIT_ID: "b2cb0da531c2f1f740045bfe7c4dac59f0b2b69c" - VERSION: "1.3.5" + VERSION: "1.3.6" NDK_VERSION: "r26d" #signing keys env variable checks ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}" diff --git a/Cargo.lock b/Cargo.lock index 5dfddf127855..21f9503858e6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5494,7 +5494,7 @@ dependencies = [ [[package]] name = "rustdesk" -version = "1.3.5" +version = "1.3.6" dependencies = [ "android-wakelock", "android_logger", @@ -5594,7 +5594,7 @@ dependencies = [ [[package]] name = "rustdesk-portable-packer" -version = "1.3.5" +version = "1.3.6" dependencies = [ "brotli", "dirs 5.0.1", diff --git a/Cargo.toml b/Cargo.toml index f2f3076bef76..f5474ae4e37e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rustdesk" -version = "1.3.5" +version = "1.3.6" authors = ["rustdesk "] edition = "2021" build= "build.rs" diff --git a/appimage/AppImageBuilder-aarch64.yml b/appimage/AppImageBuilder-aarch64.yml index 64792efe9a5d..1e8e39596065 100644 --- a/appimage/AppImageBuilder-aarch64.yml +++ b/appimage/AppImageBuilder-aarch64.yml @@ -18,7 +18,7 @@ AppDir: id: rustdesk name: rustdesk icon: rustdesk - version: 1.3.5 + version: 1.3.6 exec: usr/share/rustdesk/rustdesk exec_args: $@ apt: diff --git a/appimage/AppImageBuilder-x86_64.yml b/appimage/AppImageBuilder-x86_64.yml index ea0db9d52cf6..646113d4a495 100644 --- a/appimage/AppImageBuilder-x86_64.yml +++ b/appimage/AppImageBuilder-x86_64.yml @@ -18,7 +18,7 @@ AppDir: id: rustdesk name: rustdesk icon: rustdesk - version: 1.3.5 + version: 1.3.6 exec: usr/share/rustdesk/rustdesk exec_args: $@ apt: diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index 1776db7a5e71..d8c20985f81c 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # 1.1.9-1 works for android, but for ios it becomes 1.1.91, need to set it to 1.1.9-a.1 for iOS, will get 1.1.9.1, but iOS store not allow 4 numbers -version: 1.3.5+54 +version: 1.3.6+55 environment: sdk: '^3.1.0' diff --git a/libs/portable/Cargo.toml b/libs/portable/Cargo.toml index b9c4447a2138..b9982bce79e0 100644 --- a/libs/portable/Cargo.toml +++ b/libs/portable/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rustdesk-portable-packer" -version = "1.3.5" +version = "1.3.6" edition = "2021" description = "RustDesk Remote Desktop" diff --git a/res/PKGBUILD b/res/PKGBUILD index 7154ad5abe1f..ab97225a3c4d 100644 --- a/res/PKGBUILD +++ b/res/PKGBUILD @@ -1,5 +1,5 @@ pkgname=rustdesk -pkgver=1.3.5 +pkgver=1.3.6 pkgrel=0 epoch= pkgdesc="" diff --git a/res/rpm-flutter-suse.spec b/res/rpm-flutter-suse.spec index 63e38acc8608..347ecd989e48 100644 --- a/res/rpm-flutter-suse.spec +++ b/res/rpm-flutter-suse.spec @@ -1,5 +1,5 @@ Name: rustdesk -Version: 1.3.5 +Version: 1.3.6 Release: 0 Summary: RPM package License: GPL-3.0 diff --git a/res/rpm-flutter.spec b/res/rpm-flutter.spec index 1359cb4100cc..b6a25ac55fa5 100644 --- a/res/rpm-flutter.spec +++ b/res/rpm-flutter.spec @@ -1,5 +1,5 @@ Name: rustdesk -Version: 1.3.5 +Version: 1.3.6 Release: 0 Summary: RPM package License: GPL-3.0 diff --git a/res/rpm.spec b/res/rpm.spec index 860d05df2a1b..90c57e673675 100644 --- a/res/rpm.spec +++ b/res/rpm.spec @@ -1,5 +1,5 @@ Name: rustdesk -Version: 1.3.5 +Version: 1.3.6 Release: 0 Summary: RPM package License: GPL-3.0 From e9c5e0d26b10c6aa2d243d31c65a27560829002c Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Sat, 21 Dec 2024 15:09:03 +0800 Subject: [PATCH 494/541] fix: android, mouse mode, right menu, unexpected click (#10330) Signed-off-by: fufesou --- flutter/lib/common/widgets/remote_input.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/flutter/lib/common/widgets/remote_input.dart b/flutter/lib/common/widgets/remote_input.dart index c31350b047c0..6eb9b0594708 100644 --- a/flutter/lib/common/widgets/remote_input.dart +++ b/flutter/lib/common/widgets/remote_input.dart @@ -339,7 +339,9 @@ class _RawTouchGestureDetectorRegionState if (isDesktop || isWebDesktop) { ffi.cursorModel.clearRemoteWindowCoords(); } - await inputModel.sendMouse('up', MouseButtons.left); + if (handleTouch) { + await inputModel.sendMouse('up', MouseButtons.left); + } } // scale + pan event From 72f5184ee0f8a551dadae3e1ea95532d8a72c4b3 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Sun, 22 Dec 2024 11:20:38 +0800 Subject: [PATCH 495/541] unused --- flutter/deploy.sh | 14 -------------- 1 file changed, 14 deletions(-) delete mode 100755 flutter/deploy.sh diff --git a/flutter/deploy.sh b/flutter/deploy.sh deleted file mode 100755 index f6826fd8720d..000000000000 --- a/flutter/deploy.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env bash -cd build/web/ -python3 -c 'x=open("./main.dart.js", "rt").read();import re;y=re.search("https://.*canvaskit-wasm@([\d\.]+)/bin/",x);dirname="canvaskit@"+y.groups()[0];z=x.replace(y.group(),"/"+dirname+"/");f=open("./main.dart.js", "wt");f.write(z);import os;os.system("ln -s canvaskit " + dirname);' -mv jds/dist/index.js ./ -mv jds/dist/vendor.js ./ -/bin/rm -rf js -python3 -c 'import hashlib;x=hashlib.sha1(open("./main.dart.js").read().encode()).hexdigest()[:10];y=open("index.html","rt").read().replace("main.dart.js", "main.dart.js?v="+x);open("index.html","wt").write(y)' -python3 -c 'import hashlib;x=hashlib.sha1(open("./index.js").read().encode()).hexdigest()[:10];y=open("index.html","rt").read().replace("js/dist/index.js", "index.js?v="+x);open("index.html","wt").write(y)' -python3 -c 'import hashlib;x=hashlib.sha1(open("./vendor.js").read().encode()).hexdigest()[:10];y=open("index.html","rt").read().replace("js/dist/vendor.js", "vendor.js?v="+x);open("index.html","wt").write(y)' -tar czf x * -scp x sg:/tmp/ -ssh sg "sudo tar xzf /tmp/x -C /var/www/html/web.rustdesk.com/ && /bin/rm /tmp/x && sudo chown www-data:www-data /var/www/html/web.rustdesk.com/ -R" -/bin/rm x -cd - From 7289dbc80f91b3bf2a52d0eec597cfbe7b116f00 Mon Sep 17 00:00:00 2001 From: RustDesk <71636191+rustdesk@users.noreply.github.com> Date: Sun, 22 Dec 2024 11:35:55 +0800 Subject: [PATCH 496/541] Update flutter-build.yml (#10337) --- .github/workflows/flutter-build.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/flutter-build.yml b/.github/workflows/flutter-build.yml index ea525f525321..fe23f9423786 100644 --- a/.github/workflows/flutter-build.yml +++ b/.github/workflows/flutter-build.yml @@ -546,6 +546,12 @@ jobs: run: | rustup target add ${{ matrix.job.target }} cargo build --features flutter,hwcodec --release --target aarch64-apple-ios --lib + + - name: Upload liblibrustdesk.a Artifacts + uses: actions/upload-artifact@master + with: + name: liblibrustdesk.a + path: target/aarch64-apple-ios/release/liblibrustdesk.a - name: Build rustdesk shell: bash From 49dabd3533a71f984553be5e5edb176cf5a72f57 Mon Sep 17 00:00:00 2001 From: Integral Date: Mon, 23 Dec 2024 20:28:04 +0800 Subject: [PATCH 497/541] refactor: replace &PathBuf with &Path to enhance generality (#10332) --- libs/clipboard/src/platform/unix/local_file.rs | 6 +++--- libs/clipboard/src/platform/unix/mod.rs | 6 +++--- libs/clipboard/src/platform/unix/ns_clipboard.rs | 7 +++++-- libs/clipboard/src/platform/unix/url.rs | 2 +- libs/clipboard/src/platform/unix/x11.rs | 7 +++++-- libs/hbb_common/src/fs.rs | 6 +++--- libs/portable/src/bin_reader.rs | 6 +++--- libs/portable/src/main.rs | 10 +++++----- src/platform/macos.rs | 6 +++--- src/platform/windows.rs | 10 ++++------ src/plugin/manager.rs | 6 +++--- src/plugin/plugins.rs | 4 ++-- src/server/portable_service.rs | 8 ++++---- 13 files changed, 44 insertions(+), 40 deletions(-) diff --git a/libs/clipboard/src/platform/unix/local_file.rs b/libs/clipboard/src/platform/unix/local_file.rs index e24712efa4d3..b609b8cc79ef 100644 --- a/libs/clipboard/src/platform/unix/local_file.rs +++ b/libs/clipboard/src/platform/unix/local_file.rs @@ -3,7 +3,7 @@ use std::{ fs::File, io::{BufRead, BufReader, Read, Seek}, os::unix::prelude::PermissionsExt, - path::PathBuf, + path::{Path, PathBuf}, sync::atomic::{AtomicU64, Ordering}, time::SystemTime, }; @@ -51,7 +51,7 @@ pub(super) struct LocalFile { } impl LocalFile { - pub fn try_open(path: &PathBuf) -> Result { + pub fn try_open(path: &Path) -> Result { let mt = std::fs::metadata(path).map_err(|e| CliprdrError::FileError { path: path.clone(), err: e, @@ -219,7 +219,7 @@ impl LocalFile { pub(super) fn construct_file_list(paths: &[PathBuf]) -> Result, CliprdrError> { fn constr_file_lst( - path: &PathBuf, + path: &Path, file_list: &mut Vec, visited: &mut HashSet, ) -> Result<(), CliprdrError> { diff --git a/libs/clipboard/src/platform/unix/mod.rs b/libs/clipboard/src/platform/unix/mod.rs index 9a086109473a..34021d6bf209 100644 --- a/libs/clipboard/src/platform/unix/mod.rs +++ b/libs/clipboard/src/platform/unix/mod.rs @@ -1,5 +1,5 @@ use std::{ - path::PathBuf, + path::{Path, PathBuf}, sync::{mpsc::Sender, Arc}, time::Duration, }; @@ -74,7 +74,7 @@ trait SysClipboard: Send + Sync { } #[cfg(target_os = "linux")] -fn get_sys_clipboard(ignore_path: &PathBuf) -> Result, CliprdrError> { +fn get_sys_clipboard(ignore_path: &Path) -> Result, CliprdrError> { #[cfg(feature = "wayland")] { unimplemented!() @@ -88,7 +88,7 @@ fn get_sys_clipboard(ignore_path: &PathBuf) -> Result, Cli } #[cfg(target_os = "macos")] -fn get_sys_clipboard(ignore_path: &PathBuf) -> Result, CliprdrError> { +fn get_sys_clipboard(ignore_path: &Path) -> Result, CliprdrError> { use ns_clipboard::*; let ns_pb = NsPasteboard::new(ignore_path)?; Ok(Box::new(ns_pb) as Box<_>) diff --git a/libs/clipboard/src/platform/unix/ns_clipboard.rs b/libs/clipboard/src/platform/unix/ns_clipboard.rs index 32c60a4643f8..a9112fe62591 100644 --- a/libs/clipboard/src/platform/unix/ns_clipboard.rs +++ b/libs/clipboard/src/platform/unix/ns_clipboard.rs @@ -1,4 +1,7 @@ -use std::{collections::BTreeSet, path::PathBuf}; +use std::{ + collections::BTreeSet, + path::{Path, PathBuf}, +}; use cacao::pasteboard::{Pasteboard, PasteboardName}; use hbb_common::log; @@ -30,7 +33,7 @@ pub struct NsPasteboard { } impl NsPasteboard { - pub fn new(ignore_path: &PathBuf) -> Result { + pub fn new(ignore_path: &Path) -> Result { Ok(Self { ignore_path: ignore_path.to_owned(), former_file_list: Mutex::new(vec![]), diff --git a/libs/clipboard/src/platform/unix/url.rs b/libs/clipboard/src/platform/unix/url.rs index 2ae520f4dfcd..126a341cd728 100644 --- a/libs/clipboard/src/platform/unix/url.rs +++ b/libs/clipboard/src/platform/unix/url.rs @@ -7,7 +7,7 @@ use crate::CliprdrError; // url encode and decode is needed const ENCODE_SET: percent_encoding::AsciiSet = percent_encoding::CONTROLS.add(b' ').remove(b'/'); -pub(super) fn encode_path_to_uri(path: &PathBuf) -> io::Result { +pub(super) fn encode_path_to_uri(path: &Path) -> io::Result { let encoded = percent_encoding::percent_encode(path.to_str()?.as_bytes(), &ENCODE_SET).to_string(); format!("file://{}", encoded) diff --git a/libs/clipboard/src/platform/unix/x11.rs b/libs/clipboard/src/platform/unix/x11.rs index 41b642640444..606ff6719961 100644 --- a/libs/clipboard/src/platform/unix/x11.rs +++ b/libs/clipboard/src/platform/unix/x11.rs @@ -1,4 +1,7 @@ -use std::{collections::BTreeSet, path::PathBuf}; +use std::{ + collections::BTreeSet, + path::{Path, PathBuf}, +}; use hbb_common::log; use once_cell::sync::OnceCell; @@ -26,7 +29,7 @@ pub struct X11Clipboard { } impl X11Clipboard { - pub fn new(ignore_path: &PathBuf) -> Result { + pub fn new(ignore_path: &Path) -> Result { let clipboard = get_clip()?; let text_uri_list = clipboard .setter diff --git a/libs/hbb_common/src/fs.rs b/libs/hbb_common/src/fs.rs index 8031516972ea..2605f304e73c 100644 --- a/libs/hbb_common/src/fs.rs +++ b/libs/hbb_common/src/fs.rs @@ -123,7 +123,7 @@ pub fn get_home_as_string() -> String { } fn read_dir_recursive( - path: &PathBuf, + path: &Path, prefix: &Path, include_hidden: bool, ) -> ResultType> { @@ -186,7 +186,7 @@ pub fn get_recursive_files(path: &str, include_hidden: bool) -> ResultType ResultType> { @@ -854,7 +854,7 @@ pub async fn handle_read_jobs( Ok(job_log) } -pub fn remove_all_empty_dir(path: &PathBuf) -> ResultType<()> { +pub fn remove_all_empty_dir(path: &Path) -> ResultType<()> { let fd = read_dir(path, true)?; for entry in fd.entries.iter() { match entry.entry_type.enum_value() { diff --git a/libs/portable/src/bin_reader.rs b/libs/portable/src/bin_reader.rs index ced5baf327ff..9effbc5893b7 100644 --- a/libs/portable/src/bin_reader.rs +++ b/libs/portable/src/bin_reader.rs @@ -1,7 +1,7 @@ use std::{ fs::{self}, io::{Cursor, Read}, - path::PathBuf, + path::Path, }; #[cfg(windows)] @@ -42,7 +42,7 @@ impl BinaryData { buf } - pub fn write_to_file(&self, prefix: &PathBuf) { + pub fn write_to_file(&self, prefix: &Path) { let p = prefix.join(&self.path); if let Some(parent) = p.parent() { if !parent.exists() { @@ -122,7 +122,7 @@ impl BinaryReader { } #[cfg(linux)] - pub fn configure_permission(&self, prefix: &PathBuf) { + pub fn configure_permission(&self, prefix: &Path) { use std::os::unix::prelude::PermissionsExt; let exe_path = prefix.join(&self.exe); diff --git a/libs/portable/src/main.rs b/libs/portable/src/main.rs index 7b68d821c816..87d4897c2d50 100644 --- a/libs/portable/src/main.rs +++ b/libs/portable/src/main.rs @@ -1,7 +1,7 @@ #![windows_subsystem = "windows"] use std::{ - path::PathBuf, + path::{Path, PathBuf}, process::{Command, Stdio}, }; @@ -22,7 +22,7 @@ const APPNAME_RUNTIME_ENV_KEY: &str = "RUSTDESK_APPNAME"; #[cfg(windows)] const SET_FOREGROUND_WINDOW_ENV_KEY: &str = "SET_FOREGROUND_WINDOW"; -fn is_timestamp_matches(dir: &PathBuf, ts: &mut u64) -> bool { +fn is_timestamp_matches(dir: &Path, ts: &mut u64) -> bool { let Ok(app_metadata) = std::str::from_utf8(APP_METADATA) else { return true; }; @@ -50,7 +50,7 @@ fn is_timestamp_matches(dir: &PathBuf, ts: &mut u64) -> bool { false } -fn write_meta(dir: &PathBuf, ts: u64) { +fn write_meta(dir: &Path, ts: u64) { let meta_file = dir.join(APP_METADATA_CONFIG); if ts != 0 { let content = format!("{}{}", META_LINE_PREFIX_TIMESTAMP, ts); @@ -169,13 +169,13 @@ fn main() { #[cfg(windows)] mod windows { - use std::{fs, os::windows::process::CommandExt, path::PathBuf, process::Command}; + use std::{fs, os::windows::process::CommandExt, path::Path, process::Command}; // Used for privacy mode(magnifier impl). pub const RUNTIME_BROKER_EXE: &'static str = "C:\\Windows\\System32\\RuntimeBroker.exe"; pub const WIN_TOPMOST_INJECTED_PROCESS_EXE: &'static str = "RuntimeBroker_rustdesk.exe"; - pub(super) fn copy_runtime_broker(dir: &PathBuf) { + pub(super) fn copy_runtime_broker(dir: &Path) { let src = RUNTIME_BROKER_EXE; let tgt = WIN_TOPMOST_INJECTED_PROCESS_EXE; let target_file = dir.join(tgt); diff --git a/src/platform/macos.rs b/src/platform/macos.rs index b3c5546a6fc9..d030bc6f1363 100644 --- a/src/platform/macos.rs +++ b/src/platform/macos.rs @@ -27,7 +27,7 @@ use include_dir::{include_dir, Dir}; use objc::rc::autoreleasepool; use objc::{class, msg_send, sel, sel_impl}; use scrap::{libc::c_void, quartz::ffi::*}; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; static PRIVILEGES_SCRIPTS_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/src/platform/privileges_scripts"); @@ -661,7 +661,7 @@ pub fn hide_dock() { } #[inline] -fn get_server_start_time_of(p: &Process, path: &PathBuf) -> Option { +fn get_server_start_time_of(p: &Process, path: &Path) -> Option { let cmd = p.cmd(); if cmd.len() <= 1 { return None; @@ -679,7 +679,7 @@ fn get_server_start_time_of(p: &Process, path: &PathBuf) -> Option { } #[inline] -fn get_server_start_time(sys: &mut System, path: &PathBuf) -> Option<(i64, Pid)> { +fn get_server_start_time(sys: &mut System, path: &Path) -> Option<(i64, Pid)> { sys.refresh_processes_specifics(ProcessRefreshKind::new()); for (_, p) in sys.processes() { if let Some(t) = get_server_start_time_of(p, path) { diff --git a/src/platform/windows.rs b/src/platform/windows.rs index c0839dc55acd..92d02220ea4a 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -1460,15 +1460,13 @@ fn to_le(v: &mut [u16]) -> &[u8] { unsafe { v.align_to().1 } } -fn get_undone_file(tmp: &PathBuf) -> ResultType { - let mut tmp1 = tmp.clone(); - tmp1.set_file_name(format!( +fn get_undone_file(tmp: &Path) -> ResultType { + Ok(tmp.with_file_name(format!( "{}.undone", tmp.file_name() .ok_or(anyhow!("Failed to get filename of {:?}", tmp))? .to_string_lossy() - )); - Ok(tmp1) + ))) } fn run_cmds(cmds: String, show: bool, tip: &str) -> ResultType<()> { @@ -1933,7 +1931,7 @@ pub fn create_process_with_logon(user: &str, pwd: &str, exe: &str, arg: &str) -> return Ok(()); } -pub fn set_path_permission(dir: &PathBuf, permission: &str) -> ResultType<()> { +pub fn set_path_permission(dir: &Path, permission: &str) -> ResultType<()> { std::process::Command::new("icacls") .arg(dir.as_os_str()) .arg("/grant") diff --git a/src/plugin/manager.rs b/src/plugin/manager.rs index 74a7f736f241..f59e4c9ff78c 100644 --- a/src/plugin/manager.rs +++ b/src/plugin/manager.rs @@ -452,7 +452,7 @@ pub(super) mod install { use std::{ fs::File, io::{BufReader, BufWriter, Write}, - path::PathBuf, + path::Path, }; use zip::ZipArchive; @@ -488,7 +488,7 @@ pub(super) mod install { Ok(()) } - fn download_file(id: &str, url: &str, filename: &PathBuf) -> bool { + fn download_file(id: &str, url: &str, filename: &Path) -> bool { let file = match File::create(filename) { Ok(f) => f, Err(e) => { @@ -505,7 +505,7 @@ pub(super) mod install { true } - fn do_install_file(filename: &PathBuf, target_dir: &PathBuf) -> ResultType<()> { + fn do_install_file(filename: &Path, target_dir: &Path) -> ResultType<()> { let mut zip = ZipArchive::new(BufReader::new(File::open(filename)?))?; for i in 0..zip.len() { let mut file = zip.by_index(i)?; diff --git a/src/plugin/plugins.rs b/src/plugin/plugins.rs index b40ee4116760..8164e19bd819 100644 --- a/src/plugin/plugins.rs +++ b/src/plugin/plugins.rs @@ -13,7 +13,7 @@ use serde_derive::Serialize; use std::{ collections::{HashMap, HashSet}, ffi::{c_char, c_void}, - path::PathBuf, + path::Path, sync::{Arc, RwLock}, }; @@ -299,7 +299,7 @@ pub(super) fn load_plugins(uninstalled_ids: &HashSet) -> ResultType<()> Ok(()) } -fn load_plugin_dir(dir: &PathBuf) { +fn load_plugin_dir(dir: &Path) { log::debug!("Begin load plugin dir: {}", dir.display()); if let Ok(rd) = std::fs::read_dir(dir) { for entry in rd { diff --git a/src/server/portable_service.rs b/src/server/portable_service.rs index ca86e48e7922..47d2f5789638 100644 --- a/src/server/portable_service.rs +++ b/src/server/portable_service.rs @@ -15,7 +15,7 @@ use shared_memory::*; use std::{ mem::size_of, ops::{Deref, DerefMut}, - path::PathBuf, + path::Path, sync::{Arc, Mutex}, time::Duration, }; @@ -92,7 +92,7 @@ impl SharedMemory { } }; log::info!("Create shared memory, size: {}, flink: {}", size, flink); - set_path_permission(&PathBuf::from(flink), "F").ok(); + set_path_permission(Path::new(&flink), "F").ok(); Ok(SharedMemory { inner: shmem }) } @@ -586,8 +586,8 @@ pub mod client { let mut exe = std::env::current_exe()?.to_string_lossy().to_string(); #[cfg(feature = "flutter")] { - if let Some(dir) = PathBuf::from(&exe).parent() { - if set_path_permission(&PathBuf::from(dir), "RX").is_err() { + if let Some(dir) = Path::new(&exe).parent() { + if set_path_permission(Path::new(dir), "RX").is_err() { *SHMEM.lock().unwrap() = None; bail!("Failed to set permission of {:?}", dir); } From 090f5b65accabdc9ce3b85888436115532c179a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jernej=20Simon=C4=8Di=C4=8D?= <1800143+jernejs@users.noreply.github.com> Date: Tue, 24 Dec 2024 07:15:22 +0100 Subject: [PATCH 498/541] Update sl.rs (#10346) --- src/lang/sl.rs | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/lang/sl.rs b/src/lang/sl.rs index fad447b692f0..6cfd29d6c1d9 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -3,7 +3,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = [ ("Status", "Stanje"), ("Your Desktop", "Vaše namizje"), - ("desk_tip", "Do vašega namizja lahko dostopate s spodnjim IDjem in geslom"), + ("desk_tip", "S spodnjim IDjem in geslom omogočite oddaljeni nadzor vašega računalnika"), ("Password", "Geslo"), ("Ready", "Pripravljen"), ("Established", "Povezava vzpostavljena"), @@ -190,7 +190,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Logging in...", "Prijavljanje..."), ("Enable RDP session sharing", "Omogoči deljenje RDP seje"), ("Auto Login", "Samodejna prijava"), - ("Enable direct IP access", "Omogoči neposredni dostop preko IP"), + ("Enable direct IP access", "Omogoči neposredni dostop preko IP naslova"), ("Rename", "Preimenuj"), ("Space", "Prazno"), ("Create desktop shortcut", "Ustvari bližnjico na namizju"), @@ -364,7 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recording", "Snemanje"), ("Directory", "Imenik"), ("Automatically record incoming sessions", "Samodejno snemaj vhodne seje"), - ("Automatically record outgoing sessions", ""), + ("Automatically record outgoing sessions", "Samodejno snemaj odhodne seje"), ("Change", "Spremeni"), ("Start session recording", "Začni snemanje seje"), ("Stop session recording", "Ustavi snemanje seje"), @@ -412,8 +412,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Select local keyboard type", "Izberite lokalno vrsto tipkovnice"), ("software_render_tip", "Če na Linuxu uporabljate Nvidino grafično kartico in se oddaljeno okno zapre takoj po vzpostavitvi povezave, lahko pomaga preklop na odprtokodni gonilnik Nouveau in uporaba programskega upodabljanja. Potreben je ponovni zagon programa."), ("Always use software rendering", "Vedno uporabi programsko upodabljanje"), - ("config_input", "Za nadzor oddaljenega namizja s tipkovnico, rabi RustDesk pravico »Nadzor vnosa«."), - ("config_microphone", "Za zajem zvoka, rabi RustDesk pravico »Snemanje zvoka«."), + ("config_input", "RustDesk potrebuje pravico »Nadzor vnosa« za nadzor oddaljenega namizja s tipkovnico."), + ("config_microphone", "RustDesk potrebuje pravico »Snemanje zvoka« za zajemanje zvoka."), ("request_elevation_tip", "Lahko tudi zaprosite za dvig pravic, če je kdo na oddaljeni strani."), ("Wait", "Čakaj"), ("Elevation Error", "Napaka pri povzdigovanju"), @@ -446,7 +446,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", "Glasovni klic"), ("Text chat", "Besedilni klepet"), ("Stop voice call", "Prekini glasovni klic"), - ("relay_hint_tip", "Morda neposredna povezava ni možna; lahko se poikusite povezati preko posrednika. Če želite uporabiti posrednika ob prvem poizkusu vzpotavljanja povezave, lahko na konec IDja dodate »/r«, ali pa izberete možnost »Vedno poveži preko posrednika« v kartici nedavnih sej, če le-ta obstja."), + ("relay_hint_tip", "Morda neposredna povezava ni možna; lahko se poizkusite povezati preko posrednika. Če želite uporabiti posrednika ob prvem poizkusu vzpotavljanja povezave, lahko na konec IDja dodate »/r«, ali pa izberete možnost »Vedno poveži preko posrednika« v kartici nedavnih sej, če le-ta obstja."), ("Reconnect", "Ponovna povezava"), ("Codec", "Kodek"), ("Resolution", "Ločljivost"), @@ -649,12 +649,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Authentication Required", "Potrebno je preverjanje pristnosti"), ("Authenticate", "Preverjanje pristnosti"), ("web_id_input_tip", "Vnesete lahko ID iz istega strežnika, neposredni dostop preko IP naslova v spletnem odjemalcu ni podprt.\nČe želite dostopati do naprave na drugem strežniku, pripnite naslov strežnika (@?key=), npr. 9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nČe želite dostopati do naprave na javnem strežniku, vnesite »@public«; ključ za javni strežnik ni potreben."), - ("Download", ""), - ("Upload folder", ""), - ("Upload files", ""), - ("Clipboard is synchronized", ""), - ("Update client clipboard", ""), - ("Untagged", ""), - ("new-version-of-{}-tip", ""), + ("Download", "Prenos"), + ("Upload folder", "Naloži mapo"), + ("Upload files", "Naloži datoteke"), + ("Clipboard is synchronized", "Odložišče je usklajeno"), + ("Update client clipboard", "Osveži odjemalčevo odložišče"), + ("Untagged", "Neoznačeno"), + ("new-version-of-{}-tip", "Na voljo je nova različica {}"), ].iter().cloned().collect(); } From 06bc554216922edbcd262426613f4b84003b1f65 Mon Sep 17 00:00:00 2001 From: XLion Date: Wed, 25 Dec 2024 00:04:34 +0800 Subject: [PATCH 499/541] Fix: DEBIAN Control md5sums (#10356) * Fix: DEBIAN Control md5 sums * I forgot import --- build.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/build.py b/build.py index f37548481975..dc85a60059df 100755 --- a/build.py +++ b/build.py @@ -9,6 +9,7 @@ import hashlib import argparse import sys +from pathlib import Path windows = platform.platform().startswith('Windows') osx = platform.platform().startswith( @@ -354,7 +355,7 @@ def build_flutter_deb(version, features): system2('mkdir -p tmpdeb/DEBIAN') generate_control_file(version) system2('cp -a ../res/DEBIAN/* tmpdeb/DEBIAN/') - md5_file('usr/share/rustdesk/files/systemd/rustdesk.service') + md5_file_folder("tmpdeb/") system2('dpkg-deb -b tmpdeb rustdesk.deb;') system2('/bin/rm -rf tmpdeb/') @@ -391,7 +392,7 @@ def build_deb_from_folder(version, binary_folder): system2('mkdir -p tmpdeb/DEBIAN') generate_control_file(version) system2('cp -a ../res/DEBIAN/* tmpdeb/DEBIAN/') - md5_file('usr/share/rustdesk/files/systemd/rustdesk.service') + md5_file_folder("tmpdeb/") system2('dpkg-deb -b tmpdeb rustdesk.deb;') system2('/bin/rm -rf tmpdeb/') @@ -624,18 +625,21 @@ def main(): system2('mkdir -p tmpdeb/usr/share/rustdesk') system2('mv tmpdeb/usr/bin/rustdesk tmpdeb/usr/share/rustdesk/') system2('cp libsciter-gtk.so tmpdeb/usr/share/rustdesk/') - md5_file('usr/share/rustdesk/files/systemd/rustdesk.service') - md5_file('etc/rustdesk/startwm.sh') - md5_file('etc/X11/rustdesk/xorg.conf') - md5_file('etc/pam.d/rustdesk') - md5_file('usr/share/rustdesk/libsciter-gtk.so') + md5_file_folder("tmpdeb/") system2('dpkg-deb -b tmpdeb rustdesk.deb; /bin/rm -rf tmpdeb/') os.rename('rustdesk.deb', 'rustdesk-%s.deb' % version) def md5_file(fn): md5 = hashlib.md5(open('tmpdeb/' + fn, 'rb').read()).hexdigest() - system2('echo "%s %s" >> tmpdeb/DEBIAN/md5sums' % (md5, fn)) + system2('echo "%s /%s" >> tmpdeb/DEBIAN/md5sums' % (md5, fn)) + +def md5_file_folder(base_dir): + base_path = Path(base_dir) + for file in base_path.rglob('*'): + if file.is_file() and 'DEBIAN' not in file.parts: + relative_path = file.relative_to(base_path) + md5_file(str(relative_path)) if __name__ == "__main__": From 9ed249966677287fe6e51cf115efff19e5e4680a Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Wed, 25 Dec 2024 15:18:06 +0800 Subject: [PATCH 500/541] fix: file clipboard, init disabled (#10361) Signed-off-by: fufesou --- src/client/io_loop.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index cbef0d9140f3..24cc60eb4814 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -310,6 +310,12 @@ impl Remote { Ok(()) }); } + + // It's better to check if the peers are windows, but it's not necessary. + #[cfg(feature = "flutter")] + if !crate::flutter::sessions::has_sessions_running(ConnType::DEFAULT_CONN) { + ContextSend::enable(false); + } } #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] @@ -1199,6 +1205,7 @@ impl Remote { let peer_platform = pi.platform.clone(); self.set_peer_info(&pi); self.handler.handle_peer_info(pi); + #[cfg(not(feature = "flutter"))] self.check_clipboard_file_context(); if !(self.handler.is_file_transfer() || self.handler.is_port_forward()) { #[cfg(feature = "flutter")] @@ -1898,6 +1905,7 @@ impl Remote { true } + #[cfg(not(feature = "flutter"))] fn check_clipboard_file_context(&self) { #[cfg(any( target_os = "windows", From 1c62a28ef390db4fc301c4df9db1979534f9af8b Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Wed, 25 Dec 2024 16:36:13 +0800 Subject: [PATCH 501/541] fix: build (#10364) Signed-off-by: fufesou --- src/client/io_loop.rs | 28 ++++++++++++++++++++++------ src/ui_session_interface.rs | 12 ------------ 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index 24cc60eb4814..bdfa8f1e2df1 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -122,6 +122,28 @@ impl Remote { } pub async fn io_loop(&mut self, key: &str, token: &str, round: u32) { + #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] + let _file_clip_context_holder = { + // `is_port_forward()` will not reach here, but we still check it for clarity. + if !self.handler.is_file_transfer() && !self.handler.is_port_forward() { + // It is ok to call this function multiple times. + ContextSend::enable(true); + Some(crate::SimpleCallOnReturn { + b: true, + f: Box::new(|| { + // No need to call `enable(false)` for sciter version, because each client of sciter version is a new process. + // It's better to check if the peers are windows(support file copy&paste), but it's not necessary. + #[cfg(feature = "flutter")] + if !crate::flutter::sessions::has_sessions_running(ConnType::DEFAULT_CONN) { + ContextSend::enable(false); + }; + }), + }) + } else { + None + } + }; + let mut last_recv_time = Instant::now(); let mut received = false; let conn_type = if self.handler.is_file_transfer() { @@ -310,12 +332,6 @@ impl Remote { Ok(()) }); } - - // It's better to check if the peers are windows, but it's not necessary. - #[cfg(feature = "flutter")] - if !crate::flutter::sessions::has_sessions_running(ConnType::DEFAULT_CONN) { - ContextSend::enable(false); - } } #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 14baa69468b6..f76bd4e944ae 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -1739,18 +1739,6 @@ impl Session { #[tokio::main(flavor = "current_thread")] pub async fn io_loop(handler: Session, round: u32) { - // It is ok to call this function multiple times. - #[cfg(any( - target_os = "windows", - all( - any(target_os = "linux", target_os = "macos"), - feature = "unix-file-copy-paste" - ) - ))] - if !handler.is_file_transfer() && !handler.is_port_forward() { - clipboard::ContextSend::enable(true); - } - #[cfg(any(target_os = "android", target_os = "ios"))] let (sender, receiver) = mpsc::unbounded_channel::(); #[cfg(not(any(target_os = "android", target_os = "ios")))] From 77baba312281f54623a692f732f3301567d001fd Mon Sep 17 00:00:00 2001 From: Vasyl Gello Date: Thu, 26 Dec 2024 09:54:46 +0200 Subject: [PATCH 502/541] Fix missing locked arg in cargo install (#10374) Signed-off-by: Vasyl Gello --- flutter/build_fdroid.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/flutter/build_fdroid.sh b/flutter/build_fdroid.sh index 1821c529afb5..40fe3c3c3513 100755 --- a/flutter/build_fdroid.sh +++ b/flutter/build_fdroid.sh @@ -237,7 +237,9 @@ prebuild) # Install rust bridge generator - cargo install cargo-expand + cargo install \ + cargo-expand \ + --locked cargo install flutter_rust_bridge_codegen \ --version "${FLUTTER_RUST_BRIDGE_VERSION}" \ --features "uuid" \ From a9f2e14091bb60d71f1af25a7f6d29115fe2d9d8 Mon Sep 17 00:00:00 2001 From: Kleofass <4000163+Kleofass@users.noreply.github.com> Date: Fri, 27 Dec 2024 08:47:01 +0200 Subject: [PATCH 503/541] Update lv.rs (#10381) --- src/lang/lv.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lang/lv.rs b/src/lang/lv.rs index 81c3bedae5f9..ae9626ff8e88 100644 --- a/src/lang/lv.rs +++ b/src/lang/lv.rs @@ -655,6 +655,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is synchronized", "Starpliktuve ir sinhronizēta"), ("Update client clipboard", "Atjaunināt klienta starpliktuvi"), ("Untagged", "Neatzīmēts"), - ("new-version-of-{}-tip", ""), + ("new-version-of-{}-tip", "Ir pieejama jauna {} versija"), ].iter().cloned().collect(); } From 39a430f96f9600a2ef19a7191d3f323c7ba4b8b5 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Sat, 28 Dec 2024 22:03:34 +0800 Subject: [PATCH 504/541] upgrade url_launch --- flutter/pubspec.lock | 8 ++++---- flutter/pubspec.yaml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index 8888f9e5734c..7a2b861c8573 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -1351,18 +1351,18 @@ packages: dependency: "direct main" description: name: url_launcher - sha256: c512655380d241a337521703af62d2c122bf7b77a46ff7dd750092aa9433499c + sha256: "9d06212b1362abc2f0f0d78e6f09f726608c74e3b9462e8368bb03314aa8d603" url: "https://pub.dev" source: hosted - version: "6.2.4" + version: "6.3.1" url_launcher_android: dependency: transitive description: name: url_launcher_android - sha256: "507dc655b1d9cb5ebc756032eb785f114e415f91557b73bf60b7e201dfedeb2f" + sha256: "6fc2f56536ee873eeb867ad176ae15f304ccccc357848b351f6f0d8d4a40d193" url: "https://pub.dev" source: hosted - version: "6.2.2" + version: "6.3.14" url_launcher_ios: dependency: "direct main" description: diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index d8c20985f81c..a64c61415ed4 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -35,7 +35,7 @@ dependencies: wakelock_plus: ^1.1.3 #firebase_analytics: ^9.1.5 package_info_plus: ^4.2.0 - url_launcher: ^6.2.1 + url_launcher: ^6.3.1 url_launcher_ios: ^6.3.2 toggle_switch: ^2.1.0 dash_chat_2: From b1f54acf90ec2c8778dd0651ebc3c1dd76563aca Mon Sep 17 00:00:00 2001 From: 21pages Date: Sun, 29 Dec 2024 23:37:52 +0800 Subject: [PATCH 505/541] fix andriod update button cannot be clicked (#10394) 1. Remove `canLaunchUrl`, which fix the issue 2. Remove `unregisterEventHandler` of `kCheckSoftwareUpdateFinish` when connection page dispose, it's registered on main. Signed-off-by: 21pages --- flutter/lib/desktop/pages/desktop_home_page.dart | 4 ---- flutter/lib/mobile/pages/connection_page.dart | 10 ++-------- 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index 691bed75c9a5..ba724eed5eee 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -834,10 +834,6 @@ class _DesktopHomePageState extends State _uniLinksSubscription?.cancel(); Get.delete(tag: 'stop-service'); _updateTimer?.cancel(); - if (!bind.isCustomClient()) { - platformFFI.unregisterEventHandler( - kCheckSoftwareUpdateFinish, kCheckSoftwareUpdateFinish); - } WidgetsBinding.instance.removeObserver(this); super.dispose(); } diff --git a/flutter/lib/mobile/pages/connection_page.dart b/flutter/lib/mobile/pages/connection_page.dart index 49e3b2c91077..2d8fe9765b24 100644 --- a/flutter/lib/mobile/pages/connection_page.dart +++ b/flutter/lib/mobile/pages/connection_page.dart @@ -107,7 +107,7 @@ class _ConnectionPageState extends State { : InkWell( onTap: () async { final url = 'https://rustdesk.com/download'; - // https://pub.dev/packages/url_launcher#configuration + // https://pub.dev/packages/url_launcher#configuration // https://developer.android.com/training/package-visibility/use-cases#open-urls-custom-tabs // // `await launchUrl(Uri.parse(url))` can also run if skip @@ -115,9 +115,7 @@ class _ConnectionPageState extends State { // 2. `` in AndroidManifest.xml // // But it is better to add the check. - if (await canLaunchUrl(Uri.parse(url))) { - await launchUrl(Uri.parse(url)); - } + await launchUrl(Uri.parse(url)); }, child: Container( alignment: AlignmentDirectional.center, @@ -370,10 +368,6 @@ class _ConnectionPageState extends State { if (Get.isRegistered()) { Get.delete(); } - if (!bind.isCustomClient()) { - platformFFI.unregisterEventHandler( - kCheckSoftwareUpdateFinish, kCheckSoftwareUpdateFinish); - } super.dispose(); } } From 8e4127b6a04ff3b465d86da5c832a6aa852fc13f Mon Sep 17 00:00:00 2001 From: rustdesk Date: Sun, 29 Dec 2024 23:43:31 +0800 Subject: [PATCH 506/541] remove all stupid canLaunchUrl --- flutter/lib/desktop/pages/desktop_setting_page.dart | 1 - flutter/lib/mobile/pages/settings_page.dart | 8 ++------ 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index d978577ab85c..f89381a3ff5b 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -607,7 +607,6 @@ class _GeneralState extends State<_General> { bool user_dir_exists = await Directory(user_dir).exists(); bool root_dir_exists = showRootDir ? await Directory(root_dir).exists() : false; - // canLaunchUrl blocked on windows portable, user SYSTEM return { 'user_dir': user_dir, 'root_dir': root_dir, diff --git a/flutter/lib/mobile/pages/settings_page.dart b/flutter/lib/mobile/pages/settings_page.dart index ede66d78ae7c..83cfa2fb23a0 100644 --- a/flutter/lib/mobile/pages/settings_page.dart +++ b/flutter/lib/mobile/pages/settings_page.dart @@ -782,9 +782,7 @@ class _SettingsState extends State with WidgetsBindingObserver { tiles: [ SettingsTile( onPressed: (context) async { - if (await canLaunchUrl(Uri.parse(url))) { - await launchUrl(Uri.parse(url)); - } + await launchUrl(Uri.parse(url)); }, title: Text(translate("Version: ") + version), value: Padding( @@ -928,9 +926,7 @@ void showAbout(OverlayDialogManager dialogManager) { InkWell( onTap: () async { const url = 'https://rustdesk.com/'; - if (await canLaunchUrl(Uri.parse(url))) { - await launchUrl(Uri.parse(url)); - } + await launchUrl(Uri.parse(url)); }, child: Padding( padding: EdgeInsets.symmetric(vertical: 8), From 98b00cdb3d44af95c1b93a9b9596b6032c5ddb8d Mon Sep 17 00:00:00 2001 From: 21pages Date: Mon, 30 Dec 2024 11:51:36 +0800 Subject: [PATCH 507/541] Fix image blur occurring at the moment of changing quality (#10399) 1. Fix this issue occurs on FFmepg qsv, FFmpeg nvenc and SDK mfx, other codecs don't have this problem. Clear cache is needed. Signed-off-by: 21pages --- Cargo.lock | 2 +- .../0009-fix-nvenc-reconfigure-blur.patch | 28 +++++++++++++++++++ res/vcpkg/ffmpeg/portfile.cmake | 1 + 3 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 res/vcpkg/ffmpeg/patch/0009-fix-nvenc-reconfigure-blur.patch diff --git a/Cargo.lock b/Cargo.lock index 21f9503858e6..a8cca450fab8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3065,7 +3065,7 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hwcodec" version = "0.7.1" -source = "git+https://github.com/rustdesk-org/hwcodec#3e7c0dc755f8a77bbed3b2a9921553a511fd7bb5" +source = "git+https://github.com/rustdesk-org/hwcodec#c4d6b1c5c4ddc7548868306004cf5d4eb614a36f" dependencies = [ "bindgen 0.59.2", "cc", diff --git a/res/vcpkg/ffmpeg/patch/0009-fix-nvenc-reconfigure-blur.patch b/res/vcpkg/ffmpeg/patch/0009-fix-nvenc-reconfigure-blur.patch new file mode 100644 index 000000000000..2e8aff64aa5a --- /dev/null +++ b/res/vcpkg/ffmpeg/patch/0009-fix-nvenc-reconfigure-blur.patch @@ -0,0 +1,28 @@ +From bec8d49e75b37806e1cff39c75027860fde0bfa2 Mon Sep 17 00:00:00 2001 +From: 21pages +Date: Fri, 27 Dec 2024 08:43:12 +0800 +Subject: [PATCH] fix nvenc reconfigure blur + +Signed-off-by: 21pages +--- + libavcodec/nvenc.c | 4 ++-- + 1 file changed, 2 insertions(+), 2 deletions(-) + +diff --git a/libavcodec/nvenc.c b/libavcodec/nvenc.c +index 2cce478be0..f4c559b7ce 100644 +--- a/libavcodec/nvenc.c ++++ b/libavcodec/nvenc.c +@@ -2741,8 +2741,8 @@ static void reconfig_encoder(AVCodecContext *avctx, const AVFrame *frame) + } + + if (reconfig_bitrate) { +- params.resetEncoder = 1; +- params.forceIDR = 1; ++ params.resetEncoder = 0; ++ params.forceIDR = 0; + + needs_encode_config = 1; + needs_reconfig = 1; +-- +2.43.0.windows.1 + diff --git a/res/vcpkg/ffmpeg/portfile.cmake b/res/vcpkg/ffmpeg/portfile.cmake index b8e05e211cbd..1d46535d5ddf 100644 --- a/res/vcpkg/ffmpeg/portfile.cmake +++ b/res/vcpkg/ffmpeg/portfile.cmake @@ -24,6 +24,7 @@ vcpkg_from_github( patch/0006-dlopen-libva.patch patch/0007-fix-linux-configure.patch patch/0008-remove-amf-loop-query.patch + patch/0009-fix-nvenc-reconfigure-blur.patch ) if(SOURCE_PATH MATCHES " ") From 4f3b821883681a6f5efbaeebfe5e26001ab36401 Mon Sep 17 00:00:00 2001 From: Dimitris Apostolou Date: Wed, 1 Jan 2025 04:15:57 +0200 Subject: [PATCH 508/541] fix: fix crate vulnerabilities (#10407) --- Cargo.lock | 44 ++++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a8cca450fab8..3247683e73d7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -723,9 +723,9 @@ checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] name = "bytemuck" -version = "1.16.1" +version = "1.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b236fc92302c97ed75b38da1f4917b5cdda4984745740f153a5d3059e48d725e" +checksum = "ef657dfab802224e671f5818e9a4935f9b1957ed18e58292690cc39e7a4092a3" [[package]] name = "byteorder" @@ -735,9 +735,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.6.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" +checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" dependencies = [ "serde 1.0.203", ] @@ -2251,9 +2251,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", "futures-sink", @@ -2261,9 +2261,9 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" [[package]] name = "futures-executor" @@ -2278,9 +2278,9 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" [[package]] name = "futures-lite" @@ -2312,9 +2312,9 @@ dependencies = [ [[package]] name = "futures-macro" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2 1.0.86", "quote 1.0.36", @@ -2323,21 +2323,21 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" [[package]] name = "futures-task" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" [[package]] name = "futures-util" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-channel", "futures-core", @@ -4382,9 +4382,9 @@ checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "openssl" -version = "0.10.64" +version = "0.10.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f" +checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5" dependencies = [ "bitflags 2.6.0", "cfg-if 1.0.0", @@ -4414,9 +4414,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" -version = "0.9.102" +version = "0.9.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c597637d56fbc83893a35eb0dd04b2b8e7a50c91e64e9493e398b5df4fb45fa2" +checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741" dependencies = [ "cc", "libc", From ef90ab2bd48801f002f1199215244e581e5733ec Mon Sep 17 00:00:00 2001 From: rustdesk Date: Wed, 1 Jan 2025 23:05:52 +0800 Subject: [PATCH 509/541] compelete fix https://github.com/rustdesk/rustdesk/discussions/10210 rather than the awful workaround --- Cargo.toml | 4 ++ build.py | 3 +- flutter/macos/Podfile.lock | 8 ++-- flutter/macos/Runner/AppDelegate.swift | 2 +- src/platform/macos.rs | 47 -------------------- src/platform/privileges_scripts/daemon.plist | 2 +- 6 files changed, 12 insertions(+), 54 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index f5474ae4e37e..28cc25cc1598 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,10 @@ crate-type = ["cdylib", "staticlib", "rlib"] name = "naming" path = "src/naming.rs" +[[bin]] +name = "service" +path = "src/service.rs" + [features] inline = [] cli = [] diff --git a/build.py b/build.py index dc85a60059df..87c0dbd3432d 100755 --- a/build.py +++ b/build.py @@ -405,12 +405,13 @@ def build_flutter_dmg(version, features): if not skip_cargo: # set minimum osx build target, now is 10.14, which is the same as the flutter xcode project system2( - f'MACOSX_DEPLOYMENT_TARGET=10.14 cargo build --features {features} --lib --release') + f'MACOSX_DEPLOYMENT_TARGET=10.14 cargo build --features {features} --release') # copy dylib system2( "cp target/release/liblibrustdesk.dylib target/release/librustdesk.dylib") os.chdir('flutter') system2('flutter build macos --release') + system2('cp -rf ../target/release/service ./build/macos/Build/Products/Release/RustDesk.app/Contents/MacOS/') ''' 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") diff --git a/flutter/macos/Podfile.lock b/flutter/macos/Podfile.lock index a29674fece3e..a9f3c7388cfb 100644 --- a/flutter/macos/Podfile.lock +++ b/flutter/macos/Podfile.lock @@ -95,17 +95,17 @@ SPEC CHECKSUMS: desktop_drop: 69eeff437544aa619c8db7f4481b3a65f7696898 desktop_multi_window: 566489c048b501134f9d7fb6a2354c60a9126486 device_info_plus: 5401765fde0b8d062a2f8eb65510fb17e77cf07f - file_selector_macos: 54fdab7caa3ac3fc43c9fac4d7d8d231277f8cf2 + file_selector_macos: 468fb6b81fac7c0e88d71317f3eec34c3b008ff9 flutter_custom_cursor: 629957115075c672287bd0fa979d863ccf6024f7 FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 package_info_plus: 02d7a575e80f194102bef286361c6c326e4c29ce - path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 + path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c screen_retriever: 59634572a57080243dd1bf715e55b6c54f241a38 sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec texture_rgba_renderer: cbed959a3c127122194a364e14b8577bd62dc8f2 uni_links_desktop: 45900fb319df48fcdea2df0756e9c2626696b026 - url_launcher_macos: 5f437abeda8c85500ceb03f5c1938a8c5a705399 - video_player_avfoundation: 7c6c11d8470e1675df7397027218274b6d2360b3 + url_launcher_macos: d2691c7dd33ed713bf3544850a623080ec693d95 + video_player_avfoundation: 02011213dab73ae3687df27ce441fbbcc82b5579 wakelock_plus: 4783562c9a43d209c458cb9b30692134af456269 window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8 window_size: 339dafa0b27a95a62a843042038fa6c3c48de195 diff --git a/flutter/macos/Runner/AppDelegate.swift b/flutter/macos/Runner/AppDelegate.swift index 3498decd37a5..46372a5822fc 100644 --- a/flutter/macos/Runner/AppDelegate.swift +++ b/flutter/macos/Runner/AppDelegate.swift @@ -1,7 +1,7 @@ import Cocoa import FlutterMacOS -@NSApplicationMain +@main class AppDelegate: FlutterAppDelegate { var launched = false; override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { diff --git a/src/platform/macos.rs b/src/platform/macos.rs index d030bc6f1363..2fb2b46db90a 100644 --- a/src/platform/macos.rs +++ b/src/platform/macos.rs @@ -515,53 +515,6 @@ pub fn lock_screen() { pub fn start_os_service() { log::info!("Username: {}", crate::username()); - let mut sys = System::new(); - let path = - std::fs::canonicalize(std::env::current_exe().unwrap_or_default()).unwrap_or_default(); - let mut server = get_server_start_time(&mut sys, &path); - if server.is_none() { - log::error!("Agent not started yet, please restart --server first to make delegate work",); - std::process::exit(-1); - } - let my_start_time = sys - .process((std::process::id() as usize).into()) - .map(|p| p.start_time()) - .unwrap_or_default() as i64; - log::info!("Startime: {my_start_time} vs {:?}", server); - - std::thread::spawn(move || loop { - std::thread::sleep(std::time::Duration::from_secs(1)); - if server.is_none() { - server = get_server_start_time(&mut sys, &path); - } - let Some((start_time, pid)) = server else { - log::error!( - "Agent not started yet, please restart --server first to make delegate work", - ); - std::process::exit(-1); - }; - if my_start_time <= start_time + 3 { - log::error!( - "Agent start later, {my_start_time} vs {start_time}, please start --server first to make delegate work, earlier more 3 seconds", - ); - std::process::exit(-1); - } - // only refresh this pid and check if valid, no need to refresh all processes since refreshing all is expensive, about 10ms on my machine - if !sys.refresh_process_specifics(pid, ProcessRefreshKind::new()) { - server = None; - continue; - } - if let Some(p) = sys.process(pid.into()) { - if let Some(p) = get_server_start_time_of(p, &path) { - server = Some((p, pid)); - } else { - server = None; - } - } else { - server = None; - } - }); - if let Err(err) = crate::ipc::start("_service") { log::error!("Failed to start ipc_service: {}", err); } diff --git a/src/platform/privileges_scripts/daemon.plist b/src/platform/privileges_scripts/daemon.plist index 61efc25eca4c..59f103a31385 100644 --- a/src/platform/privileges_scripts/daemon.plist +++ b/src/platform/privileges_scripts/daemon.plist @@ -12,7 +12,7 @@ /bin/sh -c - sleep 3; if pgrep -f '/Applications/RustDesk.app/Contents/MacOS/RustDesk --server' > /dev/null; then /Applications/RustDesk.app/Contents/MacOS/RustDesk --service; fi + /Applications/RustDesk.app/Contents/MacOS/service RunAtLoad From 7c2d62237fb3c5e9ce6dd1175eb724c85361af93 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Wed, 1 Jan 2025 23:11:38 +0800 Subject: [PATCH 510/541] missed file --- src/service.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 src/service.rs diff --git a/src/service.rs b/src/service.rs new file mode 100644 index 000000000000..ce1855bdb8bf --- /dev/null +++ b/src/service.rs @@ -0,0 +1,11 @@ +use librustdesk::*; + +#[cfg(not(target_os = "macos"))] +fn main() {} + +#[cfg(target_os = "macos")] +fn main() { + crate::common::load_custom_client(); + hbb_common::init_log(false, "service"); + crate::start_os_service(); +} From 40999c3211bec6420679142372f5b53bca112ecf Mon Sep 17 00:00:00 2001 From: 21pages Date: Thu, 2 Jan 2025 22:19:30 +0800 Subject: [PATCH 511/541] fix ffmpeg videotoolbox wrong log (#10413) * Fix ffmpeg videotoolbox wrong log when changing bitrate * Let qsv support abr, and it's safe for qsv to changing bitrate. Signed-off-by: 21pages --- libs/scrap/src/common/hwcodec.rs | 2 +- res/vcpkg/ffmpeg/patch/0004-videotoolbox-changing-bitrate.patch | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/libs/scrap/src/common/hwcodec.rs b/libs/scrap/src/common/hwcodec.rs index add4b73d4952..e4e301066311 100644 --- a/libs/scrap/src/common/hwcodec.rs +++ b/libs/scrap/src/common/hwcodec.rs @@ -193,7 +193,7 @@ impl EncoderApi for HwRamEncoder { } fn support_abr(&self) -> bool { - ["qsv", "vaapi"].iter().all(|&x| !self.config.name.contains(x)) + ["vaapi"].iter().all(|&x| !self.config.name.contains(x)) } fn support_changing_quality(&self) -> bool { diff --git a/res/vcpkg/ffmpeg/patch/0004-videotoolbox-changing-bitrate.patch b/res/vcpkg/ffmpeg/patch/0004-videotoolbox-changing-bitrate.patch index 58cf2993fdf2..77b41a7ada83 100644 --- a/res/vcpkg/ffmpeg/patch/0004-videotoolbox-changing-bitrate.patch +++ b/res/vcpkg/ffmpeg/patch/0004-videotoolbox-changing-bitrate.patch @@ -58,7 +58,7 @@ index da7b291b03..3c866177f5 100644 + int status = VTSessionSetProperty(vtctx->session, + kVTCompressionPropertyKey_AverageBitRate, + bit_rate_num); -+ if (!status) { ++ if (status) { + av_log(avctx, AV_LOG_ERROR, "Error: cannot set average bit rate: %d\n", status); + } + } From 0dbd3094ec2fe7152e1524b2e395a4e1f40de717 Mon Sep 17 00:00:00 2001 From: Xiaobo Liu Date: Mon, 6 Jan 2025 18:20:18 +0800 Subject: [PATCH 512/541] hbb_common: simplify is_compressed_file (#10436) * hbb_common: simplify is_compressed_file Signed-off-by: Xiaobo Liu * `exts` rename to `compressed_exts` --------- Signed-off-by: Xiaobo Liu --- libs/hbb_common/src/fs.rs | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/libs/hbb_common/src/fs.rs b/libs/hbb_common/src/fs.rs index 2605f304e73c..1488ffd93cf7 100644 --- a/libs/hbb_common/src/fs.rs +++ b/libs/hbb_common/src/fs.rs @@ -303,16 +303,9 @@ fn get_ext(name: &str) -> &str { #[inline] fn is_compressed_file(name: &str) -> bool { + let compressed_exts = ["xz", "gz", "zip", "7z", "rar", "bz2", "tgz", "png", "jpg"]; let ext = get_ext(name); - ext == "xz" - || ext == "gz" - || ext == "zip" - || ext == "7z" - || ext == "rar" - || ext == "bz2" - || ext == "tgz" - || ext == "png" - || ext == "jpg" + compressed_exts.contains(&ext) } impl TransferJob { From 4a3c11e71137e3359669566bf49cd565cdd83a2f Mon Sep 17 00:00:00 2001 From: Xiaobo Liu Date: Tue, 7 Jan 2025 11:14:20 +0800 Subject: [PATCH 513/541] scrap: fixed build warnning (#10442) ```shell warning: elided lifetime has a name --> src/common/mod.rs:192:21 | 187 | pub fn to<'a>( | -- lifetime `'a` declared here ... 192 | ) -> ResultType { | ^^^^^^^^^^^ this elided lifetime gets resolved as `'a` | = note: `#[warn(elided_named_lifetimes)]` on by default ``` --- libs/scrap/src/common/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/scrap/src/common/mod.rs b/libs/scrap/src/common/mod.rs index ee96f57c8514..6b163d6e4604 100644 --- a/libs/scrap/src/common/mod.rs +++ b/libs/scrap/src/common/mod.rs @@ -189,7 +189,7 @@ impl Frame<'_> { yuvfmt: EncodeYuvFormat, yuv: &'a mut Vec, mid_data: &mut Vec, - ) -> ResultType { + ) -> ResultType> { match self { Frame::PixelBuffer(pixelbuffer) => { convert_to_yuv(&pixelbuffer, yuvfmt, yuv, mid_data)?; From 8f329ebc1abe420b9d8ffc8398a6f6b271581ea2 Mon Sep 17 00:00:00 2001 From: Xiaobo Liu Date: Tue, 7 Jan 2025 11:21:43 +0800 Subject: [PATCH 514/541] scrap: style (#10445) --- libs/scrap/src/common/mod.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/libs/scrap/src/common/mod.rs b/libs/scrap/src/common/mod.rs index 6b163d6e4604..cef718cc109d 100644 --- a/libs/scrap/src/common/mod.rs +++ b/libs/scrap/src/common/mod.rs @@ -13,12 +13,12 @@ cfg_if! { } else if #[cfg(x11)] { cfg_if! { if #[cfg(feature="wayland")] { - mod linux; - mod wayland; - mod x11; - pub use self::linux::*; - pub use self::wayland::set_map_err; - pub use self::x11::PixelBuffer; + mod linux; + mod wayland; + mod x11; + pub use self::linux::*; + pub use self::wayland::set_map_err; + pub use self::x11::PixelBuffer; } else { mod x11; pub use self::x11::*; From f96c759cf5f701617fc32234c81d32f5b89bd622 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Tue, 7 Jan 2025 11:52:43 +0800 Subject: [PATCH 515/541] fix https://github.com/rustdesk/rustdesk/issues/10440 --- flutter/lib/desktop/pages/connection_page.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index 0ae7affbcf7e..235e2185a063 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -39,7 +39,7 @@ class _OnlineStatusWidgetState extends State { double? get height => bind.isIncomingOnly() ? null : em * 3; void onUsePublicServerGuide() { - const url = "https://rustdesk.com/pricing.html"; + const url = "https://rustdesk.com/pricing"; canLaunchUrlString(url).then((can) { if (can) { launchUrlString(url); From f9915df926f8d0c432e233ea2a78c75b4cca668e Mon Sep 17 00:00:00 2001 From: rustdesk Date: Wed, 8 Jan 2025 00:23:17 +0800 Subject: [PATCH 516/541] update readme --- README.md | 3 --- docs/README-PL.md | 3 --- docs/README-UA.md | 3 --- 3 files changed, 9 deletions(-) diff --git a/README.md b/README.md index b5980bd7a20a..cc555270ac85 100644 --- a/README.md +++ b/README.md @@ -175,6 +175,3 @@ Please ensure that you are running these commands from the root of the RustDesk ![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) diff --git a/docs/README-PL.md b/docs/README-PL.md index 295564457b86..ef8f42648c05 100644 --- a/docs/README-PL.md +++ b/docs/README-PL.md @@ -165,6 +165,3 @@ Upewnij się, że uruchamiasz te polecenia z katalogu głównego repozytorium Ru ![image](https://user-images.githubusercontent.com/71636191/135385039-38fdbd72-379a-422d-b97f-33df71fb1cec.png) -## [Serwery publiczne](#public-servers) - -RustDesk jest obsługiwany przez bezpłatne serwer w Unii Europejskiej, uprzejmie dostarczony przez [Codext GmbH](https://codext.link/rustdesk?utm_source=github) diff --git a/docs/README-UA.md b/docs/README-UA.md index 8f226914d709..98f19d4e69d7 100644 --- a/docs/README-UA.md +++ b/docs/README-UA.md @@ -172,6 +172,3 @@ target/release/rustdesk ![Тунелювання TCP](https://github.com/rustdesk/rustdesk/assets/28412477/78e8708f-e87e-4570-8373-1360033ea6c5) -## [Публічні сервери](#публічні-сервери) - -RustDesk підтримується безкоштовним європейським сервером, любʼязно наданим [Codext GmbH](https://codext.link/rustdesk?utm_source=github) From be5037bd038614117aaa4272569a0a5addf45366 Mon Sep 17 00:00:00 2001 From: add-uos <164976197+add-uos@users.noreply.github.com> Date: Wed, 8 Jan 2025 14:16:16 +0800 Subject: [PATCH 517/541] fix: [translations] Add the translation in tw.rs (#10452) Add the translation in tw.rs Log: Add the translation in tw.rs --- src/lang/tw.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 89854769ea42..d7eb8dc69fd7 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -364,7 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recording", "錄製"), ("Directory", "路徑"), ("Automatically record incoming sessions", "自動錄製連入的工作階段"), - ("Automatically record outgoing sessions", ""), + ("Automatically record outgoing sessions", "自動錄製連出的工作階段"), ("Change", "變更"), ("Start session recording", "開始錄影"), ("Stop session recording", "停止錄影"), From 08cdf7134d2dbe48f681db720909e2bd266ad1c3 Mon Sep 17 00:00:00 2001 From: flusheDData <116861809+flusheDData@users.noreply.github.com> Date: Fri, 10 Jan 2025 14:14:59 +0100 Subject: [PATCH 518/541] Update es.rs (#10468) --- src/lang/es.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lang/es.rs b/src/lang/es.rs index a3437e01f0b5..3ad77afe590a 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -654,7 +654,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload files", "Subir archivos"), ("Clipboard is synchronized", "Portapapeles sincronizado"), ("Update client clipboard", "Actualizar portapapeles del cliente"), - ("Untagged", ""), - ("new-version-of-{}-tip", ""), + ("Untagged", "Sin itiquetar"), + ("new-version-of-{}-tip", "Hay una nueva versión de {} disponible"), ].iter().cloned().collect(); } From b5d54debce85529aa35ebb801c17bdf116b142dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=98=AF=E6=9E=9C=E5=AE=9D=E5=91=90?= <125613427+guobao2333@users.noreply.github.com> Date: Tue, 14 Jan 2025 22:16:17 +0800 Subject: [PATCH 519/541] Fix a translation error (#10500) --- src/lang/cn.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lang/cn.rs b/src/lang/cn.rs index a3e3666c680b..18bc5e6381ed 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -346,7 +346,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Dark", "黑暗"), ("Light", "明亮"), ("Follow System", "跟随系统"), - ("Enable hardware codec", "使能硬件编解码"), + ("Enable hardware codec", "启用硬件编解码"), ("Unlock Security Settings", "解锁安全设置"), ("Enable audio", "允许传输音频"), ("Unlock Network Settings", "解锁网络设置"), From 222dbf12cddd048034dabc94ad2ef034d8cbe0e8 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Wed, 15 Jan 2025 18:24:50 +0800 Subject: [PATCH 520/541] fix: mobile, don't reset canvas on metrics changed (#10463) Signed-off-by: fufesou --- flutter/lib/models/model.dart | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 5a5dcf623eed..748fa1c4043d 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -1500,13 +1500,15 @@ class CanvasModel with ChangeNotifier { return max(bottom - MediaQueryData.fromView(ui.window).padding.top, 0); } + updateSize() => _size = getSize(); + updateViewStyle({refreshMousePos = true, notify = true}) async { final style = await bind.sessionGetViewStyle(sessionId: sessionId); if (style == null) { return; } - _size = getSize(); + updateSize(); final displayWidth = getDisplayWidth(); final displayHeight = getDisplayHeight(); final viewStyle = ViewStyle( @@ -1543,7 +1545,7 @@ class CanvasModel with ChangeNotifier { _resetCanvasOffset(int displayWidth, int displayHeight) { _x = (size.width - displayWidth * _scale) / 2; _y = (size.height - displayHeight * _scale) / 2; - if (isMobile && _lastViewStyle.style == kRemoteViewStyleOriginal) { + if (isMobile) { _moveToCenterCursor(); } } @@ -1736,7 +1738,8 @@ class CanvasModel with ChangeNotifier { _timerMobileFocusCanvasCursor?.cancel(); _timerMobileFocusCanvasCursor = Timer(Duration(milliseconds: 100), () async { - await updateViewStyle(refreshMousePos: false, notify: false); + updateSize(); + _resetCanvasOffset(getDisplayWidth(), getDisplayHeight()); notifyListeners(); }); } From dd004f1a2d91cac774ecfe61ac22d28f1df5694b Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Fri, 17 Jan 2025 02:27:20 +0800 Subject: [PATCH 521/541] fix: clipboard, client side, update is required on conn (#10464) Signed-off-by: fufesou --- src/client/io_loop.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index bdfa8f1e2df1..fb7cba3c51c3 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -1260,7 +1260,8 @@ impl Remote { // to-do: Android, is `sync_init_clipboard` really needed? // https://github.com/rustdesk/rustdesk/discussions/9010 - #[cfg(target_os = "android")] + #[cfg(feature = "flutter")] + #[cfg(not(target_os = "ios"))] crate::flutter::update_text_clipboard_required(); // on connection established client From 4b066b1fbaa8d5d6f9b53cb1e5b25e484f229da1 Mon Sep 17 00:00:00 2001 From: Samuel FORESTIER Date: Mon, 20 Jan 2025 00:59:40 +0100 Subject: [PATCH 522/541] fix(debian): makes postinst/prerm scripts idempotent (#10541) * fix(debian): makes `postinst` script idempotent * fix(debian): makes `prerm` script idempotent --- res/DEBIAN/postinst | 2 +- res/DEBIAN/prerm | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/res/DEBIAN/postinst b/res/DEBIAN/postinst index 5f642daac454..dad333ee5b9d 100755 --- a/res/DEBIAN/postinst +++ b/res/DEBIAN/postinst @@ -5,7 +5,7 @@ set -e if [ "$1" = configure ]; then INITSYS=$(ls -al /proc/1/exe | awk -F' ' '{print $NF}' | awk -F'/' '{print $NF}') - ln -s /usr/share/rustdesk/rustdesk /usr/bin/rustdesk + ln -f -s /usr/share/rustdesk/rustdesk /usr/bin/rustdesk if [ "systemd" == "$INITSYS" ]; then diff --git a/res/DEBIAN/prerm b/res/DEBIAN/prerm index baef2e2e202c..133ff11debd7 100755 --- a/res/DEBIAN/prerm +++ b/res/DEBIAN/prerm @@ -5,7 +5,7 @@ set -e case $1 in remove|upgrade) INITSYS=$(ls -al /proc/1/exe | awk -F' ' '{print $NF}' | awk -F'/' '{print $NF}') - rm /usr/bin/rustdesk + rm -f /usr/bin/rustdesk if [ "systemd" == "${INITSYS}" ]; then From c44803f5b09cd865fc36073ef7d8d65b71efda57 Mon Sep 17 00:00:00 2001 From: 21pages Date: Mon, 20 Jan 2025 17:33:41 +0800 Subject: [PATCH 523/541] replace hbb_common with submodule (#10543) Signed-off-by: 21pages --- .github/workflows/bridge.yml | 2 + .github/workflows/ci.yml | 4 + .github/workflows/flutter-build.yml | 29 + .github/workflows/playground.yml | 4 +- .gitmodules | 3 + Cargo.lock | 12 + flutter/lib/common.dart | 2 +- flutter/lib/mobile/pages/connection_page.dart | 2 +- libs/hbb_common | 1 + libs/hbb_common/.gitignore | 3 - libs/hbb_common/Cargo.toml | 65 - libs/hbb_common/build.rs | 14 - libs/hbb_common/examples/config.rs | 5 - libs/hbb_common/examples/system_message.rs | 20 - libs/hbb_common/protos/message.proto | 861 ------ libs/hbb_common/protos/rendezvous.proto | 196 -- libs/hbb_common/src/bytes_codec.rs | 280 -- libs/hbb_common/src/compress.rs | 34 - libs/hbb_common/src/config.rs | 2692 ----------------- libs/hbb_common/src/fs.rs | 953 ------ libs/hbb_common/src/keyboard.rs | 39 - libs/hbb_common/src/lib.rs | 500 --- libs/hbb_common/src/mem.rs | 14 - libs/hbb_common/src/password_security.rs | 295 -- libs/hbb_common/src/platform/linux.rs | 300 -- libs/hbb_common/src/platform/macos.rs | 55 - libs/hbb_common/src/platform/mod.rs | 81 - libs/hbb_common/src/platform/windows.rs | 198 -- libs/hbb_common/src/protos/mod.rs | 1 - libs/hbb_common/src/proxy.rs | 561 ---- libs/hbb_common/src/socket_client.rs | 291 -- libs/hbb_common/src/tcp.rs | 341 --- libs/hbb_common/src/udp.rs | 170 -- src/client.rs | 7 +- src/common.rs | 23 +- src/server/connection.rs | 2 +- 36 files changed, 71 insertions(+), 7989 deletions(-) create mode 100644 .gitmodules create mode 160000 libs/hbb_common delete mode 100644 libs/hbb_common/.gitignore delete mode 100644 libs/hbb_common/Cargo.toml delete mode 100644 libs/hbb_common/build.rs delete mode 100644 libs/hbb_common/examples/config.rs delete mode 100644 libs/hbb_common/examples/system_message.rs delete mode 100644 libs/hbb_common/protos/message.proto delete mode 100644 libs/hbb_common/protos/rendezvous.proto delete mode 100644 libs/hbb_common/src/bytes_codec.rs delete mode 100644 libs/hbb_common/src/compress.rs delete mode 100644 libs/hbb_common/src/config.rs delete mode 100644 libs/hbb_common/src/fs.rs delete mode 100644 libs/hbb_common/src/keyboard.rs delete mode 100644 libs/hbb_common/src/lib.rs delete mode 100644 libs/hbb_common/src/mem.rs delete mode 100644 libs/hbb_common/src/password_security.rs delete mode 100644 libs/hbb_common/src/platform/linux.rs delete mode 100644 libs/hbb_common/src/platform/macos.rs delete mode 100644 libs/hbb_common/src/platform/mod.rs delete mode 100644 libs/hbb_common/src/platform/windows.rs delete mode 100644 libs/hbb_common/src/protos/mod.rs delete mode 100644 libs/hbb_common/src/proxy.rs delete mode 100644 libs/hbb_common/src/socket_client.rs delete mode 100644 libs/hbb_common/src/tcp.rs delete mode 100644 libs/hbb_common/src/udp.rs diff --git a/.github/workflows/bridge.yml b/.github/workflows/bridge.yml index 9d56fbe6f2b5..5e38de4a9376 100644 --- a/.github/workflows/bridge.yml +++ b/.github/workflows/bridge.yml @@ -25,6 +25,8 @@ jobs: steps: - name: Checkout source code uses: actions/checkout@v4 + with: + submodules: recursive - name: Install prerequisites run: | diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0f9ae95d57b1..690e3cf44fb6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -45,6 +45,8 @@ jobs: # steps: # - name: Checkout source code # uses: actions/checkout@v3 + # with: + # submodules: recursive # - name: Install rust toolchain (v${{ env.MIN_SUPPORTED_RUST_VERSION }}) # uses: actions-rs/toolchain@v1 @@ -92,6 +94,8 @@ jobs: - name: Checkout source code uses: actions/checkout@v4 + with: + submodules: recursive - name: Install prerequisites shell: bash diff --git a/.github/workflows/flutter-build.yml b/.github/workflows/flutter-build.yml index fe23f9423786..30b75af64788 100644 --- a/.github/workflows/flutter-build.yml +++ b/.github/workflows/flutter-build.yml @@ -87,6 +87,8 @@ jobs: - name: Checkout source code uses: actions/checkout@v4 + with: + submodules: recursive - name: Restore bridge files uses: actions/download-artifact@master @@ -276,6 +278,8 @@ jobs: - name: Checkout source code uses: actions/checkout@v4 + with: + submodules: recursive - name: Install LLVM and Clang uses: rustdesk-org/install-llvm-action-32bit@master @@ -404,6 +408,8 @@ jobs: - name: Checkout source code uses: actions/checkout@v4 + with: + submodules: recursive - name: Restore bridge files uses: actions/download-artifact@master @@ -489,6 +495,9 @@ jobs: brew install nasm yasm - name: Checkout source code uses: actions/checkout@v4 + with: + submodules: recursive + - name: Install flutter uses: subosito/flutter-action@v2 with: @@ -594,6 +603,8 @@ jobs: - name: Checkout source code uses: actions/checkout@v4 + with: + submodules: recursive # $VCPKG_ROOT/vcpkg install --triplet arm64-ios --x-install-root="$VCPKG_ROOT/installed" @@ -666,6 +677,8 @@ jobs: - name: Checkout source code uses: actions/checkout@v4 + with: + submodules: recursive - name: Import the codesign cert if: env.MACOS_P12_BASE64 != null @@ -958,6 +971,9 @@ jobs: - name: Checkout source code uses: actions/checkout@v4 + with: + submodules: recursive + - name: Install flutter uses: subosito/flutter-action@v2 with: @@ -1234,6 +1250,9 @@ jobs: - name: Checkout source code uses: actions/checkout@v4 + with: + submodules: recursive + - name: Install flutter uses: subosito/flutter-action@v2 with: @@ -1402,6 +1421,8 @@ jobs: - name: Checkout source code uses: actions/checkout@v4 + with: + submodules: recursive - name: Set Swap Space if: ${{ matrix.job.arch == 'x86_64' }} @@ -1730,6 +1751,8 @@ jobs: - name: Checkout source code uses: actions/checkout@v4 + with: + submodules: recursive - name: Free Space run: | @@ -1920,6 +1943,8 @@ jobs: steps: - name: Checkout source code uses: actions/checkout@v4 + with: + submodules: recursive - name: Download Binary uses: actions/download-artifact@master @@ -1992,6 +2017,8 @@ jobs: steps: - name: Checkout source code uses: actions/checkout@v4 + with: + submodules: recursive - name: Download Binary uses: actions/download-artifact@master @@ -2049,6 +2076,8 @@ jobs: steps: - name: Checkout source code uses: actions/checkout@v4 + with: + submodules: recursive - name: Prepare env run: | diff --git a/.github/workflows/playground.yml b/.github/workflows/playground.yml index 46a6dd0c30f5..b0b7a57258f1 100644 --- a/.github/workflows/playground.yml +++ b/.github/workflows/playground.yml @@ -90,7 +90,8 @@ jobs: uses: actions/checkout@v3 with: ref: ${{ matrix.job.ref }} - + submodules: recursive + - name: Import the codesign cert if: env.MACOS_P12_BASE64 != null uses: apple-actions/import-codesign-certs@v1 @@ -250,6 +251,7 @@ jobs: uses: actions/checkout@v3 with: ref: ${{ matrix.job.ref }} + submodules: recursive - name: Install dependencies run: | diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 000000000000..d80e69aa84a4 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "libs/hbb_common"] + path = libs/hbb_common + url = https://github.com/rustdesk/hbb_common diff --git a/Cargo.lock b/Cargo.lock index 3247683e73d7..6fab0e8fa2e6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1581,6 +1581,16 @@ dependencies = [ "windows 0.32.0", ] +[[package]] +name = "default_net" +version = "0.1.0" +source = "git+https://github.com/rustdesk-org/default_net#a831d47bcacb4615b394968287697924a8f62be1" +dependencies = [ + "anyhow", + "regex", + "winapi 0.3.9", +] + [[package]] name = "deranged" version = "0.3.11" @@ -2901,6 +2911,7 @@ dependencies = [ "bytes", "chrono", "confy", + "default_net", "directories-next", "dirs-next", "dlopen", @@ -2925,6 +2936,7 @@ dependencies = [ "serde 1.0.203", "serde_derive", "serde_json 1.0.118", + "sha2", "socket2 0.3.19", "sodiumoxide", "sysinfo", diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 7f978201ae21..2fde813bcdba 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -3610,7 +3610,7 @@ void earlyAssert() { } void checkUpdate() { - if (isDesktop || isAndroid) { + if (!isWeb) { if (!bind.isCustomClient()) { platformFFI.registerEventHandler( kCheckSoftwareUpdateFinish, kCheckSoftwareUpdateFinish, diff --git a/flutter/lib/mobile/pages/connection_page.dart b/flutter/lib/mobile/pages/connection_page.dart index 2d8fe9765b24..1d83b5744c34 100644 --- a/flutter/lib/mobile/pages/connection_page.dart +++ b/flutter/lib/mobile/pages/connection_page.dart @@ -80,7 +80,7 @@ class _ConnectionPageState extends State { slivers: [ SliverList( delegate: SliverChildListDelegate([ - if (!bind.isCustomClient()) + if (!bind.isCustomClient() && !isIOS) Obx(() => _buildUpdateUI(stateGlobal.updateUrl.value)), _buildRemoteIDTextField(), ])), diff --git a/libs/hbb_common b/libs/hbb_common new file mode 160000 index 000000000000..49c6b24a7a8c --- /dev/null +++ b/libs/hbb_common @@ -0,0 +1 @@ +Subproject commit 49c6b24a7a8c39d4448e07b743007ef1a3febd43 diff --git a/libs/hbb_common/.gitignore b/libs/hbb_common/.gitignore deleted file mode 100644 index 693699042b1a..000000000000 --- a/libs/hbb_common/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -/target -**/*.rs.bk -Cargo.lock diff --git a/libs/hbb_common/Cargo.toml b/libs/hbb_common/Cargo.toml deleted file mode 100644 index 259d01e9dd4b..000000000000 --- a/libs/hbb_common/Cargo.toml +++ /dev/null @@ -1,65 +0,0 @@ -[package] -name = "hbb_common" -version = "0.1.0" -authors = ["open-trade "] -edition = "2018" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -flexi_logger = { version = "0.27", features = ["async"] } -protobuf = { version = "3.4", features = ["with-bytes"] } -tokio = { version = "1.38", features = ["full"] } -tokio-util = { version = "0.7", features = ["full"] } -futures = "0.3" -bytes = { version = "1.6", features = ["serde"] } -log = "0.4" -env_logger = "0.10" -socket2 = { version = "0.3", features = ["reuseport"] } -zstd = "0.13" -anyhow = "1.0" -futures-util = "0.3" -directories-next = "2.0" -rand = "0.8" -serde_derive = "1.0" -serde = "1.0" -serde_json = "1.0" -lazy_static = "1.4" -confy = { git = "https://github.com/rustdesk-org/confy" } -dirs-next = "2.0" -filetime = "0.2" -sodiumoxide = "0.2" -regex = "1.8" -tokio-socks = { git = "https://github.com/rustdesk-org/tokio-socks" } -chrono = "0.4" -backtrace = "0.3" -libc = "0.2" -dlopen = "0.1" -toml = "0.7" -uuid = { version = "1.3", features = ["v4"] } -# new sysinfo issue: https://github.com/rustdesk/rustdesk/pull/6330#issuecomment-2270871442 -sysinfo = { git = "https://github.com/rustdesk-org/sysinfo", branch = "rlim_max" } -thiserror = "1.0" -httparse = "1.5" -base64 = "0.22" -url = "2.2" - -[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] -mac_address = "1.1" -machine-uid = { git = "https://github.com/rustdesk-org/machine-uid" } -[target.'cfg(not(any(target_os = "macos", target_os = "windows")))'.dependencies] -tokio-rustls = { version = "0.26", features = ["logging", "tls12", "ring"], default-features = false } -rustls-platform-verifier = "0.3.1" -rustls-pki-types = "1.4" -[target.'cfg(any(target_os = "macos", target_os = "windows"))'.dependencies] -tokio-native-tls ="0.3" - -[build-dependencies] -protobuf-codegen = { version = "3.4" } - -[target.'cfg(target_os = "windows")'.dependencies] -winapi = { version = "0.3", features = ["winuser", "synchapi", "pdh", "memoryapi", "sysinfoapi"] } - -[target.'cfg(target_os = "macos")'.dependencies] -osascript = "0.3" - diff --git a/libs/hbb_common/build.rs b/libs/hbb_common/build.rs deleted file mode 100644 index 5ebc3a28706f..000000000000 --- a/libs/hbb_common/build.rs +++ /dev/null @@ -1,14 +0,0 @@ -fn main() { - let out_dir = format!("{}/protos", std::env::var("OUT_DIR").unwrap()); - - std::fs::create_dir_all(&out_dir).unwrap(); - - protobuf_codegen::Codegen::new() - .pure() - .out_dir(out_dir) - .inputs(["protos/rendezvous.proto", "protos/message.proto"]) - .include("protos") - .customize(protobuf_codegen::Customize::default().tokio_bytes(true)) - .run() - .expect("Codegen failed."); -} diff --git a/libs/hbb_common/examples/config.rs b/libs/hbb_common/examples/config.rs deleted file mode 100644 index 95169df8e2cb..000000000000 --- a/libs/hbb_common/examples/config.rs +++ /dev/null @@ -1,5 +0,0 @@ -extern crate hbb_common; - -fn main() { - println!("{:?}", hbb_common::config::PeerConfig::load("455058072")); -} diff --git a/libs/hbb_common/examples/system_message.rs b/libs/hbb_common/examples/system_message.rs deleted file mode 100644 index 0be788428681..000000000000 --- a/libs/hbb_common/examples/system_message.rs +++ /dev/null @@ -1,20 +0,0 @@ -extern crate hbb_common; -#[cfg(target_os = "linux")] -use hbb_common::platform::linux; -#[cfg(target_os = "macos")] -use hbb_common::platform::macos; - -fn main() { - #[cfg(target_os = "linux")] - let res = linux::system_message("test title", "test message", true); - #[cfg(target_os = "macos")] - let res = macos::alert( - "System Preferences".to_owned(), - "warning".to_owned(), - "test title".to_owned(), - "test message".to_owned(), - ["Ok".to_owned()].to_vec(), - ); - #[cfg(any(target_os = "linux", target_os = "macos"))] - println!("result {:?}", &res); -} diff --git a/libs/hbb_common/protos/message.proto b/libs/hbb_common/protos/message.proto deleted file mode 100644 index d4601c0f98e9..000000000000 --- a/libs/hbb_common/protos/message.proto +++ /dev/null @@ -1,861 +0,0 @@ -syntax = "proto3"; -package hbb; - -message EncodedVideoFrame { - bytes data = 1; - bool key = 2; - int64 pts = 3; -} - -message EncodedVideoFrames { repeated EncodedVideoFrame frames = 1; } - -message RGB { bool compress = 1; } - -// planes data send directly in binary for better use arraybuffer on web -message YUV { - bool compress = 1; - int32 stride = 2; -} - -enum Chroma { - I420 = 0; - I444 = 1; -} - -message VideoFrame { - oneof union { - EncodedVideoFrames vp9s = 6; - RGB rgb = 7; - YUV yuv = 8; - EncodedVideoFrames h264s = 10; - EncodedVideoFrames h265s = 11; - EncodedVideoFrames vp8s = 12; - EncodedVideoFrames av1s = 13; - } - int32 display = 14; -} - -message IdPk { - string id = 1; - bytes pk = 2; -} - -message DisplayInfo { - sint32 x = 1; - sint32 y = 2; - int32 width = 3; - int32 height = 4; - string name = 5; - bool online = 6; - bool cursor_embedded = 7; - Resolution original_resolution = 8; - double scale = 9; -} - -message PortForward { - string host = 1; - int32 port = 2; -} - -message FileTransfer { - string dir = 1; - bool show_hidden = 2; -} - -message OSLogin { - string username = 1; - string password = 2; -} - -message LoginRequest { - string username = 1; - bytes password = 2; - string my_id = 4; - string my_name = 5; - OptionMessage option = 6; - oneof union { - FileTransfer file_transfer = 7; - PortForward port_forward = 8; - } - bool video_ack_required = 9; - uint64 session_id = 10; - string version = 11; - OSLogin os_login = 12; - string my_platform = 13; - bytes hwid = 14; -} - -message Auth2FA { - string code = 1; - bytes hwid = 2; -} - -message ChatMessage { string text = 1; } - -message Features { - bool privacy_mode = 1; -} - -message CodecAbility { - bool vp8 = 1; - bool vp9 = 2; - bool av1 = 3; - bool h264 = 4; - bool h265 = 5; -} - -message SupportedEncoding { - bool h264 = 1; - bool h265 = 2; - bool vp8 = 3; - bool av1 = 4; - CodecAbility i444 = 5; -} - -message PeerInfo { - string username = 1; - string hostname = 2; - string platform = 3; - repeated DisplayInfo displays = 4; - int32 current_display = 5; - bool sas_enabled = 6; - string version = 7; - Features features = 9; - SupportedEncoding encoding = 10; - SupportedResolutions resolutions = 11; - // Use JSON's key-value format which is friendly for peer to handle. - // NOTE: Only support one-level dictionaries (for peer to update), and the key is of type string. - string platform_additions = 12; - WindowsSessions windows_sessions = 13; -} - -message WindowsSession { - uint32 sid = 1; - string name = 2; -} - -message LoginResponse { - oneof union { - string error = 1; - PeerInfo peer_info = 2; - } - bool enable_trusted_devices = 3; -} - -message TouchScaleUpdate { - // The delta scale factor relative to the previous scale. - // delta * 1000 - // 0 means scale end - int32 scale = 1; -} - -message TouchPanStart { - int32 x = 1; - int32 y = 2; -} - -message TouchPanUpdate { - // The delta x position relative to the previous position. - int32 x = 1; - // The delta y position relative to the previous position. - int32 y = 2; -} - -message TouchPanEnd { - int32 x = 1; - int32 y = 2; -} - -message TouchEvent { - oneof union { - TouchScaleUpdate scale_update = 1; - TouchPanStart pan_start = 2; - TouchPanUpdate pan_update = 3; - TouchPanEnd pan_end = 4; - } -} - -message PointerDeviceEvent { - oneof union { - TouchEvent touch_event = 1; - } - repeated ControlKey modifiers = 2; -} - -message MouseEvent { - int32 mask = 1; - sint32 x = 2; - sint32 y = 3; - repeated ControlKey modifiers = 4; -} - -enum KeyboardMode{ - Legacy = 0; - Map = 1; - Translate = 2; - Auto = 3; -} - -enum ControlKey { - Unknown = 0; - Alt = 1; - Backspace = 2; - CapsLock = 3; - Control = 4; - Delete = 5; - DownArrow = 6; - End = 7; - Escape = 8; - F1 = 9; - F10 = 10; - F11 = 11; - F12 = 12; - F2 = 13; - F3 = 14; - F4 = 15; - F5 = 16; - F6 = 17; - F7 = 18; - F8 = 19; - F9 = 20; - Home = 21; - LeftArrow = 22; - /// meta key (also known as "windows"; "super"; and "command") - Meta = 23; - /// option key on macOS (alt key on Linux and Windows) - Option = 24; // deprecated, use Alt instead - PageDown = 25; - PageUp = 26; - Return = 27; - RightArrow = 28; - Shift = 29; - Space = 30; - Tab = 31; - UpArrow = 32; - Numpad0 = 33; - Numpad1 = 34; - Numpad2 = 35; - Numpad3 = 36; - Numpad4 = 37; - Numpad5 = 38; - Numpad6 = 39; - Numpad7 = 40; - Numpad8 = 41; - Numpad9 = 42; - Cancel = 43; - Clear = 44; - Menu = 45; // deprecated, use Alt instead - Pause = 46; - Kana = 47; - Hangul = 48; - Junja = 49; - Final = 50; - Hanja = 51; - Kanji = 52; - Convert = 53; - Select = 54; - Print = 55; - Execute = 56; - Snapshot = 57; - Insert = 58; - Help = 59; - Sleep = 60; - Separator = 61; - Scroll = 62; - NumLock = 63; - RWin = 64; - Apps = 65; - Multiply = 66; - Add = 67; - Subtract = 68; - Decimal = 69; - Divide = 70; - Equals = 71; - NumpadEnter = 72; - RShift = 73; - RControl = 74; - RAlt = 75; - VolumeMute = 76; // mainly used on mobile devices as controlled side - VolumeUp = 77; - VolumeDown = 78; - Power = 79; // mainly used on mobile devices as controlled side - CtrlAltDel = 100; - LockScreen = 101; -} - -message KeyEvent { - // `down` indicates the key's state(down or up). - bool down = 1; - // `press` indicates a click event(down and up). - bool press = 2; - oneof union { - ControlKey control_key = 3; - // position key code. win: scancode, linux: key code, macos: key code - uint32 chr = 4; - uint32 unicode = 5; - string seq = 6; - // high word. virtual keycode - // low word. unicode - uint32 win2win_hotkey = 7; - } - repeated ControlKey modifiers = 8; - KeyboardMode mode = 9; -} - -message CursorData { - uint64 id = 1; - sint32 hotx = 2; - sint32 hoty = 3; - int32 width = 4; - int32 height = 5; - bytes colors = 6; -} - -message CursorPosition { - sint32 x = 1; - sint32 y = 2; -} - -message Hash { - string salt = 1; - string challenge = 2; -} - -enum ClipboardFormat { - Text = 0; - Rtf = 1; - Html = 2; - ImageRgba = 21; - ImagePng = 22; - ImageSvg = 23; - Special = 31; -} - -message Clipboard { - bool compress = 1; - bytes content = 2; - int32 width = 3; - int32 height = 4; - ClipboardFormat format = 5; - // Special format name, only used when format is Special. - string special_name = 6; -} - -message MultiClipboards { repeated Clipboard clipboards = 1; } - -enum FileType { - Dir = 0; - DirLink = 2; - DirDrive = 3; - File = 4; - FileLink = 5; -} - -message FileEntry { - FileType entry_type = 1; - string name = 2; - bool is_hidden = 3; - uint64 size = 4; - uint64 modified_time = 5; -} - -message FileDirectory { - int32 id = 1; - string path = 2; - repeated FileEntry entries = 3; -} - -message ReadDir { - string path = 1; - bool include_hidden = 2; -} - -message ReadEmptyDirs { - string path = 1; - bool include_hidden = 2; -} - -message ReadEmptyDirsResponse { - string path = 1; - repeated FileDirectory empty_dirs = 2; -} - -message ReadAllFiles { - int32 id = 1; - string path = 2; - bool include_hidden = 3; -} - -message FileRename { - int32 id = 1; - string path = 2; - string new_name = 3; -} - -message FileAction { - oneof union { - ReadDir read_dir = 1; - FileTransferSendRequest send = 2; - FileTransferReceiveRequest receive = 3; - FileDirCreate create = 4; - FileRemoveDir remove_dir = 5; - FileRemoveFile remove_file = 6; - ReadAllFiles all_files = 7; - FileTransferCancel cancel = 8; - FileTransferSendConfirmRequest send_confirm = 9; - FileRename rename = 10; - ReadEmptyDirs read_empty_dirs = 11; - } -} - -message FileTransferCancel { int32 id = 1; } - -message FileResponse { - oneof union { - FileDirectory dir = 1; - FileTransferBlock block = 2; - FileTransferError error = 3; - FileTransferDone done = 4; - FileTransferDigest digest = 5; - ReadEmptyDirsResponse empty_dirs = 6; - } -} - -message FileTransferDigest { - int32 id = 1; - sint32 file_num = 2; - uint64 last_modified = 3; - uint64 file_size = 4; - bool is_upload = 5; - bool is_identical = 6; -} - -message FileTransferBlock { - int32 id = 1; - sint32 file_num = 2; - bytes data = 3; - bool compressed = 4; - uint32 blk_id = 5; -} - -message FileTransferError { - int32 id = 1; - string error = 2; - sint32 file_num = 3; -} - -message FileTransferSendRequest { - int32 id = 1; - string path = 2; - bool include_hidden = 3; - int32 file_num = 4; -} - -message FileTransferSendConfirmRequest { - int32 id = 1; - sint32 file_num = 2; - oneof union { - bool skip = 3; - uint32 offset_blk = 4; - } -} - -message FileTransferDone { - int32 id = 1; - sint32 file_num = 2; -} - -message FileTransferReceiveRequest { - int32 id = 1; - string path = 2; // path written to - repeated FileEntry files = 3; - int32 file_num = 4; - uint64 total_size = 5; -} - -message FileRemoveDir { - int32 id = 1; - string path = 2; - bool recursive = 3; -} - -message FileRemoveFile { - int32 id = 1; - string path = 2; - sint32 file_num = 3; -} - -message FileDirCreate { - int32 id = 1; - string path = 2; -} - -// main logic from freeRDP -message CliprdrMonitorReady { -} - -message CliprdrFormat { - int32 id = 2; - string format = 3; -} - -message CliprdrServerFormatList { - repeated CliprdrFormat formats = 2; -} - -message CliprdrServerFormatListResponse { - int32 msg_flags = 2; -} - -message CliprdrServerFormatDataRequest { - int32 requested_format_id = 2; -} - -message CliprdrServerFormatDataResponse { - int32 msg_flags = 2; - bytes format_data = 3; -} - -message CliprdrFileContentsRequest { - int32 stream_id = 2; - int32 list_index = 3; - int32 dw_flags = 4; - int32 n_position_low = 5; - int32 n_position_high = 6; - int32 cb_requested = 7; - bool have_clip_data_id = 8; - int32 clip_data_id = 9; -} - -message CliprdrFileContentsResponse { - int32 msg_flags = 3; - int32 stream_id = 4; - bytes requested_data = 5; -} - -message Cliprdr { - oneof union { - CliprdrMonitorReady ready = 1; - CliprdrServerFormatList format_list = 2; - CliprdrServerFormatListResponse format_list_response = 3; - CliprdrServerFormatDataRequest format_data_request = 4; - CliprdrServerFormatDataResponse format_data_response = 5; - CliprdrFileContentsRequest file_contents_request = 6; - CliprdrFileContentsResponse file_contents_response = 7; - } -} - -message Resolution { - int32 width = 1; - int32 height = 2; -} - -message DisplayResolution { - int32 display = 1; - Resolution resolution = 2; -} - -message SupportedResolutions { repeated Resolution resolutions = 1; } - -message SwitchDisplay { - int32 display = 1; - sint32 x = 2; - sint32 y = 3; - int32 width = 4; - int32 height = 5; - bool cursor_embedded = 6; - SupportedResolutions resolutions = 7; - // Do not care about the origin point for now. - Resolution original_resolution = 8; -} - -message CaptureDisplays { - repeated int32 add = 1; - repeated int32 sub = 2; - repeated int32 set = 3; -} - -message ToggleVirtualDisplay { - int32 display = 1; - bool on = 2; -} - -message TogglePrivacyMode { - string impl_key = 1; - bool on = 2; -} - -message PermissionInfo { - enum Permission { - Keyboard = 0; - Clipboard = 2; - Audio = 3; - File = 4; - Restart = 5; - Recording = 6; - BlockInput = 7; - } - - Permission permission = 1; - bool enabled = 2; -} - -enum ImageQuality { - NotSet = 0; - Low = 2; - Balanced = 3; - Best = 4; -} - -message SupportedDecoding { - enum PreferCodec { - Auto = 0; - VP9 = 1; - H264 = 2; - H265 = 3; - VP8 = 4; - AV1 = 5; - } - - int32 ability_vp9 = 1; - int32 ability_h264 = 2; - int32 ability_h265 = 3; - PreferCodec prefer = 4; - int32 ability_vp8 = 5; - int32 ability_av1 = 6; - CodecAbility i444 = 7; - Chroma prefer_chroma = 8; -} - -message OptionMessage { - enum BoolOption { - NotSet = 0; - No = 1; - Yes = 2; - } - ImageQuality image_quality = 1; - BoolOption lock_after_session_end = 2; - BoolOption show_remote_cursor = 3; - BoolOption privacy_mode = 4; - BoolOption block_input = 5; - int32 custom_image_quality = 6; - BoolOption disable_audio = 7; - BoolOption disable_clipboard = 8; - BoolOption enable_file_transfer = 9; - SupportedDecoding supported_decoding = 10; - int32 custom_fps = 11; - BoolOption disable_keyboard = 12; -// Position 13 is used for Resolution. Remove later. -// Resolution custom_resolution = 13; -// BoolOption support_windows_specific_session = 14; - // starting from 15 please, do not use removed fields - BoolOption follow_remote_cursor = 15; - BoolOption follow_remote_window = 16; -} - -message TestDelay { - int64 time = 1; - bool from_client = 2; - uint32 last_delay = 3; - uint32 target_bitrate = 4; -} - -message PublicKey { - bytes asymmetric_value = 1; - bytes symmetric_value = 2; -} - -message SignedId { bytes id = 1; } - -message AudioFormat { - uint32 sample_rate = 1; - uint32 channels = 2; -} - -message AudioFrame { - bytes data = 1; -} - -// Notify peer to show message box. -message MessageBox { - // Message type. Refer to flutter/lib/common.dart/msgBox(). - string msgtype = 1; - string title = 2; - // English - string text = 3; - // If not empty, msgbox provides a button to following the link. - // The link here can't be directly http url. - // It must be the key of http url configed in peer side or "rustdesk://*" (jump in app). - string link = 4; -} - -message BackNotification { - // no need to consider block input by someone else - enum BlockInputState { - BlkStateUnknown = 0; - BlkOnSucceeded = 2; - BlkOnFailed = 3; - BlkOffSucceeded = 4; - BlkOffFailed = 5; - } - enum PrivacyModeState { - PrvStateUnknown = 0; - // Privacy mode on by someone else - PrvOnByOther = 2; - // Privacy mode is not supported on the remote side - PrvNotSupported = 3; - // Privacy mode on by self - PrvOnSucceeded = 4; - // Privacy mode on by self, but denied - PrvOnFailedDenied = 5; - // Some plugins are not found - PrvOnFailedPlugin = 6; - // Privacy mode on by self, but failed - PrvOnFailed = 7; - // Privacy mode off by self - PrvOffSucceeded = 8; - // Ctrl + P - PrvOffByPeer = 9; - // Privacy mode off by self, but failed - PrvOffFailed = 10; - PrvOffUnknown = 11; - } - - oneof union { - PrivacyModeState privacy_mode_state = 1; - BlockInputState block_input_state = 2; - } - // Supplementary message, for "PrvOnFailed" and "PrvOffFailed" - string details = 3; - // The key of the implementation - string impl_key = 4; -} - -message ElevationRequestWithLogon { - string username = 1; - string password = 2; -} - -message ElevationRequest { - oneof union { - bool direct = 1; - ElevationRequestWithLogon logon = 2; - } -} - -message SwitchSidesRequest { - bytes uuid = 1; -} - -message SwitchSidesResponse { - bytes uuid = 1; - LoginRequest lr = 2; -} - -message SwitchBack {} - -message PluginRequest { - string id = 1; - bytes content = 2; -} - -message PluginFailure { - string id = 1; - string name = 2; - string msg = 3; -} - -message WindowsSessions { - repeated WindowsSession sessions = 1; - uint32 current_sid = 2; -} - -// Query messages from peer. -message MessageQuery { - // The SwitchDisplay message of the target display. - // If the target display is not found, the message will be ignored. - int32 switch_display = 1; -} - -message Misc { - oneof union { - ChatMessage chat_message = 4; - SwitchDisplay switch_display = 5; - PermissionInfo permission_info = 6; - OptionMessage option = 7; - AudioFormat audio_format = 8; - string close_reason = 9; - bool refresh_video = 10; - bool video_received = 12; - BackNotification back_notification = 13; - bool restart_remote_device = 14; - bool uac = 15; - bool foreground_window_elevated = 16; - bool stop_service = 17; - ElevationRequest elevation_request = 18; - string elevation_response = 19; - bool portable_service_running = 20; - SwitchSidesRequest switch_sides_request = 21; - SwitchBack switch_back = 22; - // Deprecated since 1.2.4, use `change_display_resolution` (36) instead. - // But we must keep it for compatibility when peer version < 1.2.4. - Resolution change_resolution = 24; - PluginRequest plugin_request = 25; - PluginFailure plugin_failure = 26; - uint32 full_speed_fps = 27; // deprecated - uint32 auto_adjust_fps = 28; - bool client_record_status = 29; - CaptureDisplays capture_displays = 30; - int32 refresh_video_display = 31; - ToggleVirtualDisplay toggle_virtual_display = 32; - TogglePrivacyMode toggle_privacy_mode = 33; - SupportedEncoding supported_encoding = 34; - uint32 selected_sid = 35; - DisplayResolution change_display_resolution = 36; - MessageQuery message_query = 37; - int32 follow_current_display = 38; - } -} - -message VoiceCallRequest { - int64 req_timestamp = 1; - // Indicates whether the request is a connect action or a disconnect action. - bool is_connect = 2; -} - -message VoiceCallResponse { - bool accepted = 1; - int64 req_timestamp = 2; // Should copy from [VoiceCallRequest::req_timestamp]. - int64 ack_timestamp = 3; -} - -message Message { - oneof union { - SignedId signed_id = 3; - PublicKey public_key = 4; - TestDelay test_delay = 5; - VideoFrame video_frame = 6; - LoginRequest login_request = 7; - LoginResponse login_response = 8; - Hash hash = 9; - MouseEvent mouse_event = 10; - AudioFrame audio_frame = 11; - CursorData cursor_data = 12; - CursorPosition cursor_position = 13; - uint64 cursor_id = 14; - KeyEvent key_event = 15; - Clipboard clipboard = 16; - FileAction file_action = 17; - FileResponse file_response = 18; - Misc misc = 19; - Cliprdr cliprdr = 20; - MessageBox message_box = 21; - SwitchSidesResponse switch_sides_response = 22; - VoiceCallRequest voice_call_request = 23; - VoiceCallResponse voice_call_response = 24; - PeerInfo peer_info = 25; - PointerDeviceEvent pointer_device_event = 26; - Auth2FA auth_2fa = 27; - MultiClipboards multi_clipboards = 28; - } -} diff --git a/libs/hbb_common/protos/rendezvous.proto b/libs/hbb_common/protos/rendezvous.proto deleted file mode 100644 index 2fc0d9040e6e..000000000000 --- a/libs/hbb_common/protos/rendezvous.proto +++ /dev/null @@ -1,196 +0,0 @@ -syntax = "proto3"; -package hbb; - -message RegisterPeer { - string id = 1; - int32 serial = 2; -} - -enum ConnType { - DEFAULT_CONN = 0; - FILE_TRANSFER = 1; - PORT_FORWARD = 2; - RDP = 3; -} - -message RegisterPeerResponse { bool request_pk = 2; } - -message PunchHoleRequest { - string id = 1; - NatType nat_type = 2; - string licence_key = 3; - ConnType conn_type = 4; - string token = 5; - string version = 6; -} - -message PunchHole { - bytes socket_addr = 1; - string relay_server = 2; - NatType nat_type = 3; -} - -message TestNatRequest { - int32 serial = 1; -} - -// per my test, uint/int has no difference in encoding, int not good for negative, use sint for negative -message TestNatResponse { - int32 port = 1; - ConfigUpdate cu = 2; // for mobile -} - -enum NatType { - UNKNOWN_NAT = 0; - ASYMMETRIC = 1; - SYMMETRIC = 2; -} - -message PunchHoleSent { - bytes socket_addr = 1; - string id = 2; - string relay_server = 3; - NatType nat_type = 4; - string version = 5; -} - -message RegisterPk { - string id = 1; - bytes uuid = 2; - bytes pk = 3; - string old_id = 4; -} - -message RegisterPkResponse { - enum Result { - OK = 0; - UUID_MISMATCH = 2; - ID_EXISTS = 3; - TOO_FREQUENT = 4; - INVALID_ID_FORMAT = 5; - NOT_SUPPORT = 6; - SERVER_ERROR = 7; - } - Result result = 1; - int32 keep_alive = 2; -} - -message PunchHoleResponse { - bytes socket_addr = 1; - bytes pk = 2; - enum Failure { - ID_NOT_EXIST = 0; - OFFLINE = 2; - LICENSE_MISMATCH = 3; - LICENSE_OVERUSE = 4; - } - Failure failure = 3; - string relay_server = 4; - oneof union { - NatType nat_type = 5; - bool is_local = 6; - } - string other_failure = 7; - int32 feedback = 8; -} - -message ConfigUpdate { - int32 serial = 1; - repeated string rendezvous_servers = 2; -} - -message RequestRelay { - string id = 1; - string uuid = 2; - bytes socket_addr = 3; - string relay_server = 4; - bool secure = 5; - string licence_key = 6; - ConnType conn_type = 7; - string token = 8; -} - -message RelayResponse { - bytes socket_addr = 1; - string uuid = 2; - string relay_server = 3; - oneof union { - string id = 4; - bytes pk = 5; - } - string refuse_reason = 6; - string version = 7; - int32 feedback = 9; -} - -message SoftwareUpdate { string url = 1; } - -// if in same intranet, punch hole won't work both for udp and tcp, -// even some router has below connection error if we connect itself, -// { kind: Other, error: "could not resolve to any address" }, -// so we request local address to connect. -message FetchLocalAddr { - bytes socket_addr = 1; - string relay_server = 2; -} - -message LocalAddr { - bytes socket_addr = 1; - bytes local_addr = 2; - string relay_server = 3; - string id = 4; - string version = 5; -} - -message PeerDiscovery { - string cmd = 1; - string mac = 2; - string id = 3; - string username = 4; - string hostname = 5; - string platform = 6; - string misc = 7; -} - -message OnlineRequest { - string id = 1; - repeated string peers = 2; -} - -message OnlineResponse { - bytes states = 1; -} - -message KeyExchange { - repeated bytes keys = 1; -} - -message HealthCheck { - string token = 1; -} - -message RendezvousMessage { - oneof union { - RegisterPeer register_peer = 6; - RegisterPeerResponse register_peer_response = 7; - PunchHoleRequest punch_hole_request = 8; - PunchHole punch_hole = 9; - PunchHoleSent punch_hole_sent = 10; - PunchHoleResponse punch_hole_response = 11; - FetchLocalAddr fetch_local_addr = 12; - LocalAddr local_addr = 13; - ConfigUpdate configure_update = 14; - RegisterPk register_pk = 15; - RegisterPkResponse register_pk_response = 16; - SoftwareUpdate software_update = 17; - RequestRelay request_relay = 18; - RelayResponse relay_response = 19; - TestNatRequest test_nat_request = 20; - TestNatResponse test_nat_response = 21; - PeerDiscovery peer_discovery = 22; - OnlineRequest online_request = 23; - OnlineResponse online_response = 24; - KeyExchange key_exchange = 25; - HealthCheck hc = 26; - } -} diff --git a/libs/hbb_common/src/bytes_codec.rs b/libs/hbb_common/src/bytes_codec.rs deleted file mode 100644 index bfc79871554a..000000000000 --- a/libs/hbb_common/src/bytes_codec.rs +++ /dev/null @@ -1,280 +0,0 @@ -use bytes::{Buf, BufMut, Bytes, BytesMut}; -use std::io; -use tokio_util::codec::{Decoder, Encoder}; - -#[derive(Debug, Clone, Copy)] -pub struct BytesCodec { - state: DecodeState, - raw: bool, - max_packet_length: usize, -} - -#[derive(Debug, Clone, Copy)] -enum DecodeState { - Head, - Data(usize), -} - -impl Default for BytesCodec { - fn default() -> Self { - Self::new() - } -} - -impl BytesCodec { - pub fn new() -> Self { - Self { - state: DecodeState::Head, - raw: false, - max_packet_length: usize::MAX, - } - } - - pub fn set_raw(&mut self) { - self.raw = true; - } - - pub fn set_max_packet_length(&mut self, n: usize) { - self.max_packet_length = n; - } - - fn decode_head(&mut self, src: &mut BytesMut) -> io::Result> { - if src.is_empty() { - return Ok(None); - } - let head_len = ((src[0] & 0x3) + 1) as usize; - if src.len() < head_len { - return Ok(None); - } - let mut n = src[0] as usize; - if head_len > 1 { - n |= (src[1] as usize) << 8; - } - if head_len > 2 { - n |= (src[2] as usize) << 16; - } - if head_len > 3 { - n |= (src[3] as usize) << 24; - } - n >>= 2; - if n > self.max_packet_length { - return Err(io::Error::new(io::ErrorKind::InvalidData, "Too big packet")); - } - src.advance(head_len); - src.reserve(n); - Ok(Some(n)) - } - - fn decode_data(&self, n: usize, src: &mut BytesMut) -> io::Result> { - if src.len() < n { - return Ok(None); - } - Ok(Some(src.split_to(n))) - } -} - -impl Decoder for BytesCodec { - type Item = BytesMut; - type Error = io::Error; - - fn decode(&mut self, src: &mut BytesMut) -> Result, io::Error> { - if self.raw { - if !src.is_empty() { - let len = src.len(); - return Ok(Some(src.split_to(len))); - } else { - return Ok(None); - } - } - let n = match self.state { - DecodeState::Head => match self.decode_head(src)? { - Some(n) => { - self.state = DecodeState::Data(n); - n - } - None => return Ok(None), - }, - DecodeState::Data(n) => n, - }; - - match self.decode_data(n, src)? { - Some(data) => { - self.state = DecodeState::Head; - Ok(Some(data)) - } - None => Ok(None), - } - } -} - -impl Encoder for BytesCodec { - type Error = io::Error; - - fn encode(&mut self, data: Bytes, buf: &mut BytesMut) -> Result<(), io::Error> { - if self.raw { - buf.reserve(data.len()); - buf.put(data); - return Ok(()); - } - if data.len() <= 0x3F { - buf.put_u8((data.len() << 2) as u8); - } else if data.len() <= 0x3FFF { - buf.put_u16_le((data.len() << 2) as u16 | 0x1); - } else if data.len() <= 0x3FFFFF { - let h = (data.len() << 2) as u32 | 0x2; - buf.put_u16_le((h & 0xFFFF) as u16); - buf.put_u8((h >> 16) as u8); - } else if data.len() <= 0x3FFFFFFF { - buf.put_u32_le((data.len() << 2) as u32 | 0x3); - } else { - return Err(io::Error::new(io::ErrorKind::InvalidInput, "Overflow")); - } - buf.extend(data); - Ok(()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - #[test] - fn test_codec1() { - let mut codec = BytesCodec::new(); - let mut buf = BytesMut::new(); - let mut bytes: Vec = Vec::new(); - bytes.resize(0x3F, 1); - assert!(codec.encode(bytes.into(), &mut buf).is_ok()); - let buf_saved = buf.clone(); - assert_eq!(buf.len(), 0x3F + 1); - if let Ok(Some(res)) = codec.decode(&mut buf) { - assert_eq!(res.len(), 0x3F); - assert_eq!(res[0], 1); - } else { - panic!(); - } - let mut codec2 = BytesCodec::new(); - let mut buf2 = BytesMut::new(); - if let Ok(None) = codec2.decode(&mut buf2) { - } else { - panic!(); - } - buf2.extend(&buf_saved[0..1]); - if let Ok(None) = codec2.decode(&mut buf2) { - } else { - panic!(); - } - buf2.extend(&buf_saved[1..]); - if let Ok(Some(res)) = codec2.decode(&mut buf2) { - assert_eq!(res.len(), 0x3F); - assert_eq!(res[0], 1); - } else { - panic!(); - } - } - - #[test] - fn test_codec2() { - let mut codec = BytesCodec::new(); - let mut buf = BytesMut::new(); - let mut bytes: Vec = Vec::new(); - assert!(codec.encode("".into(), &mut buf).is_ok()); - assert_eq!(buf.len(), 1); - bytes.resize(0x3F + 1, 2); - assert!(codec.encode(bytes.into(), &mut buf).is_ok()); - assert_eq!(buf.len(), 0x3F + 2 + 2); - if let Ok(Some(res)) = codec.decode(&mut buf) { - assert_eq!(res.len(), 0); - } else { - panic!(); - } - if let Ok(Some(res)) = codec.decode(&mut buf) { - assert_eq!(res.len(), 0x3F + 1); - assert_eq!(res[0], 2); - } else { - panic!(); - } - } - - #[test] - fn test_codec3() { - let mut codec = BytesCodec::new(); - let mut buf = BytesMut::new(); - let mut bytes: Vec = Vec::new(); - bytes.resize(0x3F - 1, 3); - assert!(codec.encode(bytes.into(), &mut buf).is_ok()); - assert_eq!(buf.len(), 0x3F + 1 - 1); - if let Ok(Some(res)) = codec.decode(&mut buf) { - assert_eq!(res.len(), 0x3F - 1); - assert_eq!(res[0], 3); - } else { - panic!(); - } - } - #[test] - fn test_codec4() { - let mut codec = BytesCodec::new(); - let mut buf = BytesMut::new(); - let mut bytes: Vec = Vec::new(); - bytes.resize(0x3FFF, 4); - assert!(codec.encode(bytes.into(), &mut buf).is_ok()); - assert_eq!(buf.len(), 0x3FFF + 2); - if let Ok(Some(res)) = codec.decode(&mut buf) { - assert_eq!(res.len(), 0x3FFF); - assert_eq!(res[0], 4); - } else { - panic!(); - } - } - - #[test] - fn test_codec5() { - let mut codec = BytesCodec::new(); - let mut buf = BytesMut::new(); - let mut bytes: Vec = Vec::new(); - bytes.resize(0x3FFFFF, 5); - assert!(codec.encode(bytes.into(), &mut buf).is_ok()); - assert_eq!(buf.len(), 0x3FFFFF + 3); - if let Ok(Some(res)) = codec.decode(&mut buf) { - assert_eq!(res.len(), 0x3FFFFF); - assert_eq!(res[0], 5); - } else { - panic!(); - } - } - - #[test] - fn test_codec6() { - let mut codec = BytesCodec::new(); - let mut buf = BytesMut::new(); - let mut bytes: Vec = Vec::new(); - bytes.resize(0x3FFFFF + 1, 6); - assert!(codec.encode(bytes.into(), &mut buf).is_ok()); - let buf_saved = buf.clone(); - assert_eq!(buf.len(), 0x3FFFFF + 4 + 1); - if let Ok(Some(res)) = codec.decode(&mut buf) { - assert_eq!(res.len(), 0x3FFFFF + 1); - assert_eq!(res[0], 6); - } else { - panic!(); - } - let mut codec2 = BytesCodec::new(); - let mut buf2 = BytesMut::new(); - buf2.extend(&buf_saved[0..1]); - if let Ok(None) = codec2.decode(&mut buf2) { - } else { - panic!(); - } - buf2.extend(&buf_saved[1..6]); - if let Ok(None) = codec2.decode(&mut buf2) { - } else { - panic!(); - } - buf2.extend(&buf_saved[6..]); - if let Ok(Some(res)) = codec2.decode(&mut buf2) { - assert_eq!(res.len(), 0x3FFFFF + 1); - assert_eq!(res[0], 6); - } else { - panic!(); - } - } -} diff --git a/libs/hbb_common/src/compress.rs b/libs/hbb_common/src/compress.rs deleted file mode 100644 index 761d916e4f83..000000000000 --- a/libs/hbb_common/src/compress.rs +++ /dev/null @@ -1,34 +0,0 @@ -use std::{cell::RefCell, io}; -use zstd::bulk::Compressor; - -// The library supports regular compression levels from 1 up to ZSTD_maxCLevel(), -// which is currently 22. Levels >= 20 -// Default level is ZSTD_CLEVEL_DEFAULT==3. -// value 0 means default, which is controlled by ZSTD_CLEVEL_DEFAULT -thread_local! { - static COMPRESSOR: RefCell>> = RefCell::new(Compressor::new(crate::config::COMPRESS_LEVEL)); -} - -pub fn compress(data: &[u8]) -> Vec { - let mut out = Vec::new(); - COMPRESSOR.with(|c| { - if let Ok(mut c) = c.try_borrow_mut() { - match &mut *c { - Ok(c) => match c.compress(data) { - Ok(res) => out = res, - Err(err) => { - crate::log::debug!("Failed to compress: {}", err); - } - }, - Err(err) => { - crate::log::debug!("Failed to get compressor: {}", err); - } - } - } - }); - out -} - -pub fn decompress(data: &[u8]) -> Vec { - zstd::decode_all(data).unwrap_or_default() -} diff --git a/libs/hbb_common/src/config.rs b/libs/hbb_common/src/config.rs deleted file mode 100644 index dd4abaf9d9b9..000000000000 --- a/libs/hbb_common/src/config.rs +++ /dev/null @@ -1,2692 +0,0 @@ -use std::{ - collections::{HashMap, HashSet}, - fs, - io::{Read, Write}, - net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}, - ops::{Deref, DerefMut}, - path::{Path, PathBuf}, - sync::{Mutex, RwLock}, - time::{Duration, Instant, SystemTime}, -}; - -use anyhow::Result; -use bytes::Bytes; -use rand::Rng; -use regex::Regex; -use serde as de; -use serde_derive::{Deserialize, Serialize}; -use serde_json; -use sodiumoxide::base64; -use sodiumoxide::crypto::sign; - -use crate::{ - compress::{compress, decompress}, - log, - password_security::{ - decrypt_str_or_original, decrypt_vec_or_original, encrypt_str_or_original, - encrypt_vec_or_original, symmetric_crypt, - }, -}; - -pub const RENDEZVOUS_TIMEOUT: u64 = 12_000; -pub const CONNECT_TIMEOUT: u64 = 18_000; -pub const READ_TIMEOUT: u64 = 18_000; -// https://github.com/quic-go/quic-go/issues/525#issuecomment-294531351 -// https://datatracker.ietf.org/doc/html/draft-hamilton-early-deployment-quic-00#section-6.10 -// 15 seconds is recommended by quic, though oneSIP recommend 25 seconds, -// https://www.onsip.com/voip-resources/voip-fundamentals/what-is-nat-keepalive -pub const REG_INTERVAL: i64 = 15_000; -pub const COMPRESS_LEVEL: i32 = 3; -const SERIAL: i32 = 3; -const PASSWORD_ENC_VERSION: &str = "00"; -pub const ENCRYPT_MAX_LEN: usize = 128; // used for password, pin, etc, not for all - -#[cfg(target_os = "macos")] -lazy_static::lazy_static! { - pub static ref ORG: RwLock = RwLock::new("com.carriez".to_owned()); -} - -type Size = (i32, i32, i32, i32); -type KeyPair = (Vec, Vec); - -lazy_static::lazy_static! { - static ref CONFIG: RwLock = RwLock::new(Config::load()); - static ref CONFIG2: RwLock = RwLock::new(Config2::load()); - static ref LOCAL_CONFIG: RwLock = RwLock::new(LocalConfig::load()); - static ref TRUSTED_DEVICES: RwLock<(Vec, bool)> = Default::default(); - static ref ONLINE: Mutex> = Default::default(); - pub static ref PROD_RENDEZVOUS_SERVER: RwLock = RwLock::new(match option_env!("RENDEZVOUS_SERVER") { - Some(key) if !key.is_empty() => key, - _ => "", - }.to_owned()); - pub static ref EXE_RENDEZVOUS_SERVER: RwLock = Default::default(); - pub static ref APP_NAME: RwLock = RwLock::new("RustDesk".to_owned()); - static ref KEY_PAIR: Mutex> = Default::default(); - static ref USER_DEFAULT_CONFIG: RwLock<(UserDefaultConfig, Instant)> = RwLock::new((UserDefaultConfig::load(), Instant::now())); - pub static ref NEW_STORED_PEER_CONFIG: Mutex> = Default::default(); - pub static ref DEFAULT_SETTINGS: RwLock> = Default::default(); - pub static ref OVERWRITE_SETTINGS: RwLock> = Default::default(); - pub static ref DEFAULT_DISPLAY_SETTINGS: RwLock> = Default::default(); - pub static ref OVERWRITE_DISPLAY_SETTINGS: RwLock> = Default::default(); - pub static ref DEFAULT_LOCAL_SETTINGS: RwLock> = Default::default(); - pub static ref OVERWRITE_LOCAL_SETTINGS: RwLock> = Default::default(); - pub static ref HARD_SETTINGS: RwLock> = Default::default(); - pub static ref BUILTIN_SETTINGS: RwLock> = Default::default(); -} - -lazy_static::lazy_static! { - pub static ref APP_DIR: RwLock = Default::default(); -} - -#[cfg(any(target_os = "android", target_os = "ios"))] -lazy_static::lazy_static! { - pub static ref APP_HOME_DIR: RwLock = Default::default(); -} - -pub const LINK_DOCS_HOME: &str = "https://rustdesk.com/docs/en/"; -pub const LINK_DOCS_X11_REQUIRED: &str = "https://rustdesk.com/docs/en/manual/linux/#x11-required"; -pub const LINK_HEADLESS_LINUX_SUPPORT: &str = - "https://github.com/rustdesk/rustdesk/wiki/Headless-Linux-Support"; -lazy_static::lazy_static! { - pub static ref HELPER_URL: HashMap<&'static str, &'static str> = HashMap::from([ - ("rustdesk docs home", LINK_DOCS_HOME), - ("rustdesk docs x11-required", LINK_DOCS_X11_REQUIRED), - ("rustdesk x11 headless", LINK_HEADLESS_LINUX_SUPPORT), - ]); -} - -const CHARS: &[char] = &[ - '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', - 'm', 'n', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', -]; - -pub const RENDEZVOUS_SERVERS: &[&str] = &["rs-ny.rustdesk.com"]; -pub const PUBLIC_RS_PUB_KEY: &str = "OeVuKk5nlHiXp+APNn0Y3pC1Iwpwn44JGqrQCsWqmBw="; - -pub const RS_PUB_KEY: &str = match option_env!("RS_PUB_KEY") { - Some(key) if !key.is_empty() => key, - _ => PUBLIC_RS_PUB_KEY, -}; - -pub const RENDEZVOUS_PORT: i32 = 21116; -pub const RELAY_PORT: i32 = 21117; - -macro_rules! serde_field_string { - ($default_func:ident, $de_func:ident, $default_expr:expr) => { - fn $default_func() -> String { - $default_expr - } - - fn $de_func<'de, D>(deserializer: D) -> Result - where - D: de::Deserializer<'de>, - { - let s: String = - de::Deserialize::deserialize(deserializer).unwrap_or(Self::$default_func()); - if s.is_empty() { - return Ok(Self::$default_func()); - } - Ok(s) - } - }; -} - -macro_rules! serde_field_bool { - ($struct_name: ident, $field_name: literal, $func: ident, $default: literal) => { - #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] - pub struct $struct_name { - #[serde(default = $default, rename = $field_name, deserialize_with = "deserialize_bool")] - pub v: bool, - } - impl Default for $struct_name { - fn default() -> Self { - Self { v: Self::$func() } - } - } - impl $struct_name { - pub fn $func() -> bool { - UserDefaultConfig::read($field_name) == "Y" - } - } - impl Deref for $struct_name { - type Target = bool; - - fn deref(&self) -> &Self::Target { - &self.v - } - } - impl DerefMut for $struct_name { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.v - } - } - }; -} - -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub enum NetworkType { - Direct, - ProxySocks, -} - -#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq)] -pub struct Config { - #[serde( - default, - skip_serializing_if = "String::is_empty", - deserialize_with = "deserialize_string" - )] - pub id: String, // use - #[serde(default, deserialize_with = "deserialize_string")] - enc_id: String, // store - #[serde(default, deserialize_with = "deserialize_string")] - password: String, - #[serde(default, deserialize_with = "deserialize_string")] - salt: String, - #[serde(default, deserialize_with = "deserialize_keypair")] - key_pair: KeyPair, // sk, pk - #[serde(default, deserialize_with = "deserialize_bool")] - key_confirmed: bool, - #[serde(default, deserialize_with = "deserialize_hashmap_string_bool")] - keys_confirmed: HashMap, -} - -#[derive(Debug, Default, PartialEq, Serialize, Deserialize, Clone)] -pub struct Socks5Server { - #[serde(default, deserialize_with = "deserialize_string")] - pub proxy: String, - #[serde(default, deserialize_with = "deserialize_string")] - pub username: String, - #[serde(default, deserialize_with = "deserialize_string")] - pub password: String, -} - -// more variable configs -#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq)] -pub struct Config2 { - #[serde(default, deserialize_with = "deserialize_string")] - rendezvous_server: String, - #[serde(default, deserialize_with = "deserialize_i32")] - nat_type: i32, - #[serde(default, deserialize_with = "deserialize_i32")] - serial: i32, - #[serde(default, deserialize_with = "deserialize_string")] - unlock_pin: String, - #[serde(default, deserialize_with = "deserialize_string")] - trusted_devices: String, - - #[serde(default)] - socks: Option, - - // the other scalar value must before this - #[serde(default, deserialize_with = "deserialize_hashmap_string_string")] - pub options: HashMap, -} - -#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq)] -pub struct Resolution { - pub w: i32, - pub h: i32, -} - -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] -pub struct PeerConfig { - #[serde(default, deserialize_with = "deserialize_vec_u8")] - pub password: Vec, - #[serde(default, deserialize_with = "deserialize_size")] - pub size: Size, - #[serde(default, deserialize_with = "deserialize_size")] - pub size_ft: Size, - #[serde(default, deserialize_with = "deserialize_size")] - pub size_pf: Size, - #[serde( - default = "PeerConfig::default_view_style", - deserialize_with = "PeerConfig::deserialize_view_style", - skip_serializing_if = "String::is_empty" - )] - pub view_style: String, - // Image scroll style, scrollbar or scroll auto - #[serde( - default = "PeerConfig::default_scroll_style", - deserialize_with = "PeerConfig::deserialize_scroll_style", - skip_serializing_if = "String::is_empty" - )] - pub scroll_style: String, - #[serde( - default = "PeerConfig::default_image_quality", - deserialize_with = "PeerConfig::deserialize_image_quality", - skip_serializing_if = "String::is_empty" - )] - pub image_quality: String, - #[serde( - default = "PeerConfig::default_custom_image_quality", - deserialize_with = "PeerConfig::deserialize_custom_image_quality", - skip_serializing_if = "Vec::is_empty" - )] - pub custom_image_quality: Vec, - #[serde(flatten)] - pub show_remote_cursor: ShowRemoteCursor, - #[serde(flatten)] - pub lock_after_session_end: LockAfterSessionEnd, - #[serde(flatten)] - pub privacy_mode: PrivacyMode, - #[serde(flatten)] - pub allow_swap_key: AllowSwapKey, - #[serde(default, deserialize_with = "deserialize_vec_i32_string_i32")] - pub port_forwards: Vec<(i32, String, i32)>, - #[serde(default, deserialize_with = "deserialize_i32")] - pub direct_failures: i32, - #[serde(flatten)] - pub disable_audio: DisableAudio, - #[serde(flatten)] - pub disable_clipboard: DisableClipboard, - #[serde(flatten)] - pub enable_file_copy_paste: EnableFileCopyPaste, - #[serde(flatten)] - pub show_quality_monitor: ShowQualityMonitor, - #[serde(flatten)] - pub follow_remote_cursor: FollowRemoteCursor, - #[serde(flatten)] - pub follow_remote_window: FollowRemoteWindow, - #[serde( - default, - deserialize_with = "deserialize_string", - skip_serializing_if = "String::is_empty" - )] - pub keyboard_mode: String, - #[serde(flatten)] - pub view_only: ViewOnly, - #[serde(flatten)] - pub sync_init_clipboard: SyncInitClipboard, - // Mouse wheel or touchpad scroll mode - #[serde( - default = "PeerConfig::default_reverse_mouse_wheel", - deserialize_with = "PeerConfig::deserialize_reverse_mouse_wheel", - skip_serializing_if = "String::is_empty" - )] - pub reverse_mouse_wheel: String, - #[serde( - default = "PeerConfig::default_displays_as_individual_windows", - deserialize_with = "PeerConfig::deserialize_displays_as_individual_windows", - skip_serializing_if = "String::is_empty" - )] - pub displays_as_individual_windows: String, - #[serde( - default = "PeerConfig::default_use_all_my_displays_for_the_remote_session", - deserialize_with = "PeerConfig::deserialize_use_all_my_displays_for_the_remote_session", - skip_serializing_if = "String::is_empty" - )] - pub use_all_my_displays_for_the_remote_session: String, - - #[serde( - default, - deserialize_with = "deserialize_hashmap_resolutions", - skip_serializing_if = "HashMap::is_empty" - )] - pub custom_resolutions: HashMap, - - // The other scalar value must before this - #[serde( - default, - deserialize_with = "deserialize_hashmap_string_string", - skip_serializing_if = "HashMap::is_empty" - )] - pub options: HashMap, // not use delete to represent default values - // Various data for flutter ui - #[serde(default, deserialize_with = "deserialize_hashmap_string_string")] - pub ui_flutter: HashMap, - #[serde(default)] - pub info: PeerInfoSerde, - #[serde(default)] - pub transfer: TransferSerde, -} - -impl Default for PeerConfig { - fn default() -> Self { - Self { - password: Default::default(), - size: Default::default(), - size_ft: Default::default(), - size_pf: Default::default(), - view_style: Self::default_view_style(), - scroll_style: Self::default_scroll_style(), - image_quality: Self::default_image_quality(), - custom_image_quality: Self::default_custom_image_quality(), - show_remote_cursor: Default::default(), - lock_after_session_end: Default::default(), - privacy_mode: Default::default(), - allow_swap_key: Default::default(), - port_forwards: Default::default(), - direct_failures: Default::default(), - disable_audio: Default::default(), - disable_clipboard: Default::default(), - enable_file_copy_paste: Default::default(), - show_quality_monitor: Default::default(), - follow_remote_cursor: Default::default(), - follow_remote_window: Default::default(), - keyboard_mode: Default::default(), - view_only: Default::default(), - reverse_mouse_wheel: Self::default_reverse_mouse_wheel(), - displays_as_individual_windows: Self::default_displays_as_individual_windows(), - use_all_my_displays_for_the_remote_session: - Self::default_use_all_my_displays_for_the_remote_session(), - custom_resolutions: Default::default(), - options: Self::default_options(), - ui_flutter: Default::default(), - info: Default::default(), - transfer: Default::default(), - sync_init_clipboard: Default::default(), - } - } -} - -#[derive(Debug, PartialEq, Default, Serialize, Deserialize, Clone)] -pub struct PeerInfoSerde { - #[serde(default, deserialize_with = "deserialize_string")] - pub username: String, - #[serde(default, deserialize_with = "deserialize_string")] - pub hostname: String, - #[serde(default, deserialize_with = "deserialize_string")] - pub platform: String, -} - -#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq)] -pub struct TransferSerde { - #[serde(default, deserialize_with = "deserialize_vec_string")] - pub write_jobs: Vec, - #[serde(default, deserialize_with = "deserialize_vec_string")] - pub read_jobs: Vec, -} - -#[inline] -pub fn get_online_state() -> i64 { - *ONLINE.lock().unwrap().values().max().unwrap_or(&0) -} - -#[cfg(not(any(target_os = "android", target_os = "ios")))] -fn patch(path: PathBuf) -> PathBuf { - if let Some(_tmp) = path.to_str() { - #[cfg(windows)] - return _tmp - .replace( - "system32\\config\\systemprofile", - "ServiceProfiles\\LocalService", - ) - .into(); - #[cfg(target_os = "macos")] - return _tmp.replace("Application Support", "Preferences").into(); - #[cfg(target_os = "linux")] - { - if _tmp == "/root" { - if let Ok(user) = crate::platform::linux::run_cmds_trim_newline("whoami") { - if user != "root" { - let cmd = format!("getent passwd '{}' | awk -F':' '{{print $6}}'", user); - if let Ok(output) = crate::platform::linux::run_cmds_trim_newline(&cmd) { - return output.into(); - } - return format!("/home/{user}").into(); - } - } - } - } - } - path -} - -impl Config2 { - fn load() -> Config2 { - let mut config = Config::load_::("2"); - let mut store = false; - if let Some(mut socks) = config.socks { - let (password, _, store2) = - decrypt_str_or_original(&socks.password, PASSWORD_ENC_VERSION); - socks.password = password; - config.socks = Some(socks); - store |= store2; - } - let (unlock_pin, _, store2) = - decrypt_str_or_original(&config.unlock_pin, PASSWORD_ENC_VERSION); - config.unlock_pin = unlock_pin; - store |= store2; - if store { - config.store(); - } - config - } - - pub fn file() -> PathBuf { - Config::file_("2") - } - - fn store(&self) { - let mut config = self.clone(); - if let Some(mut socks) = config.socks { - socks.password = - encrypt_str_or_original(&socks.password, PASSWORD_ENC_VERSION, ENCRYPT_MAX_LEN); - config.socks = Some(socks); - } - config.unlock_pin = - encrypt_str_or_original(&config.unlock_pin, PASSWORD_ENC_VERSION, ENCRYPT_MAX_LEN); - Config::store_(&config, "2"); - } - - pub fn get() -> Config2 { - return CONFIG2.read().unwrap().clone(); - } - - pub fn set(cfg: Config2) -> bool { - let mut lock = CONFIG2.write().unwrap(); - if *lock == cfg { - return false; - } - *lock = cfg; - lock.store(); - true - } -} - -pub fn load_path( - file: PathBuf, -) -> T { - let cfg = match confy::load_path(&file) { - Ok(config) => config, - Err(err) => { - if let confy::ConfyError::GeneralLoadError(err) = &err { - if err.kind() == std::io::ErrorKind::NotFound { - return T::default(); - } - } - log::error!("Failed to load config '{}': {}", file.display(), err); - T::default() - } - }; - cfg -} - -#[inline] -pub fn store_path(path: PathBuf, cfg: T) -> crate::ResultType<()> { - #[cfg(not(windows))] - { - use std::os::unix::fs::PermissionsExt; - Ok(confy::store_path_perms( - path, - cfg, - fs::Permissions::from_mode(0o600), - )?) - } - #[cfg(windows)] - { - Ok(confy::store_path(path, cfg)?) - } -} - -impl Config { - fn load_( - suffix: &str, - ) -> T { - let file = Self::file_(suffix); - let cfg = load_path(file); - if suffix.is_empty() { - log::trace!("{:?}", cfg); - } - cfg - } - - fn store_(config: &T, suffix: &str) { - let file = Self::file_(suffix); - if let Err(err) = store_path(file, config) { - log::error!("Failed to store {suffix} config: {err}"); - } - } - - fn load() -> Config { - let mut config = Config::load_::(""); - let mut store = false; - let (password, _, store1) = decrypt_str_or_original(&config.password, PASSWORD_ENC_VERSION); - config.password = password; - store |= store1; - let mut id_valid = false; - let (id, encrypted, store2) = decrypt_str_or_original(&config.enc_id, PASSWORD_ENC_VERSION); - if encrypted { - config.id = id; - id_valid = true; - store |= store2; - } else if - // Comment out for forward compatible - // crate::get_modified_time(&Self::file_("")) - // .checked_sub(std::time::Duration::from_secs(30)) // allow modification during installation - // .unwrap_or_else(crate::get_exe_time) - // < crate::get_exe_time() - // && - !config.id.is_empty() - && config.enc_id.is_empty() - && !decrypt_str_or_original(&config.id, PASSWORD_ENC_VERSION).1 - { - id_valid = true; - store = true; - } - if !id_valid { - for _ in 0..3 { - if let Some(id) = Config::get_auto_id() { - config.id = id; - store = true; - break; - } else { - log::error!("Failed to generate new id"); - } - } - } - if store { - config.store(); - } - config - } - - fn store(&self) { - let mut config = self.clone(); - config.password = - encrypt_str_or_original(&config.password, PASSWORD_ENC_VERSION, ENCRYPT_MAX_LEN); - config.enc_id = encrypt_str_or_original(&config.id, PASSWORD_ENC_VERSION, ENCRYPT_MAX_LEN); - config.id = "".to_owned(); - Config::store_(&config, ""); - } - - pub fn file() -> PathBuf { - Self::file_("") - } - - fn file_(suffix: &str) -> PathBuf { - let name = format!("{}{}", *APP_NAME.read().unwrap(), suffix); - Config::with_extension(Self::path(name)) - } - - pub fn is_empty(&self) -> bool { - (self.id.is_empty() && self.enc_id.is_empty()) || self.key_pair.0.is_empty() - } - - pub fn get_home() -> PathBuf { - #[cfg(any(target_os = "android", target_os = "ios"))] - return PathBuf::from(APP_HOME_DIR.read().unwrap().as_str()); - #[cfg(not(any(target_os = "android", target_os = "ios")))] - { - if let Some(path) = dirs_next::home_dir() { - patch(path) - } else if let Ok(path) = std::env::current_dir() { - path - } else { - std::env::temp_dir() - } - } - } - - pub fn path>(p: P) -> PathBuf { - #[cfg(any(target_os = "android", target_os = "ios"))] - { - let mut path: PathBuf = APP_DIR.read().unwrap().clone().into(); - path.push(p); - return path; - } - #[cfg(not(any(target_os = "android", target_os = "ios")))] - { - #[cfg(not(target_os = "macos"))] - let org = "".to_owned(); - #[cfg(target_os = "macos")] - let org = ORG.read().unwrap().clone(); - // /var/root for root - if let Some(project) = - directories_next::ProjectDirs::from("", &org, &APP_NAME.read().unwrap()) - { - let mut path = patch(project.config_dir().to_path_buf()); - path.push(p); - return path; - } - "".into() - } - } - - #[allow(unreachable_code)] - pub fn log_path() -> PathBuf { - #[cfg(target_os = "macos")] - { - if let Some(path) = dirs_next::home_dir().as_mut() { - path.push(format!("Library/Logs/{}", *APP_NAME.read().unwrap())); - return path.clone(); - } - } - #[cfg(target_os = "linux")] - { - let mut path = Self::get_home(); - path.push(format!(".local/share/logs/{}", *APP_NAME.read().unwrap())); - std::fs::create_dir_all(&path).ok(); - return path; - } - #[cfg(target_os = "android")] - { - let mut path = Self::get_home(); - path.push(format!("{}/Logs", *APP_NAME.read().unwrap())); - std::fs::create_dir_all(&path).ok(); - return path; - } - if let Some(path) = Self::path("").parent() { - let mut path: PathBuf = path.into(); - path.push("log"); - return path; - } - "".into() - } - - pub fn ipc_path(postfix: &str) -> String { - #[cfg(windows)] - { - // \\ServerName\pipe\PipeName - // where ServerName is either the name of a remote computer or a period, to specify the local computer. - // https://docs.microsoft.com/en-us/windows/win32/ipc/pipe-names - format!( - "\\\\.\\pipe\\{}\\query{}", - *APP_NAME.read().unwrap(), - postfix - ) - } - #[cfg(not(windows))] - { - use std::os::unix::fs::PermissionsExt; - #[cfg(target_os = "android")] - let mut path: PathBuf = - format!("{}/{}", *APP_DIR.read().unwrap(), *APP_NAME.read().unwrap()).into(); - #[cfg(not(target_os = "android"))] - let mut path: PathBuf = format!("/tmp/{}", *APP_NAME.read().unwrap()).into(); - fs::create_dir(&path).ok(); - fs::set_permissions(&path, fs::Permissions::from_mode(0o0777)).ok(); - path.push(format!("ipc{postfix}")); - path.to_str().unwrap_or("").to_owned() - } - } - - pub fn icon_path() -> PathBuf { - let mut path = Self::path("icons"); - if fs::create_dir_all(&path).is_err() { - path = std::env::temp_dir(); - } - path - } - - #[inline] - pub fn get_any_listen_addr(is_ipv4: bool) -> SocketAddr { - if is_ipv4 { - SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0) - } else { - SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0) - } - } - - pub fn get_rendezvous_server() -> String { - let mut rendezvous_server = EXE_RENDEZVOUS_SERVER.read().unwrap().clone(); - if rendezvous_server.is_empty() { - rendezvous_server = Self::get_option("custom-rendezvous-server"); - } - if rendezvous_server.is_empty() { - rendezvous_server = PROD_RENDEZVOUS_SERVER.read().unwrap().clone(); - } - if rendezvous_server.is_empty() { - rendezvous_server = CONFIG2.read().unwrap().rendezvous_server.clone(); - } - if rendezvous_server.is_empty() { - rendezvous_server = Self::get_rendezvous_servers() - .drain(..) - .next() - .unwrap_or_default(); - } - if !rendezvous_server.contains(':') { - rendezvous_server = format!("{rendezvous_server}:{RENDEZVOUS_PORT}"); - } - rendezvous_server - } - - pub fn get_rendezvous_servers() -> Vec { - let s = EXE_RENDEZVOUS_SERVER.read().unwrap().clone(); - if !s.is_empty() { - return vec![s]; - } - let s = Self::get_option("custom-rendezvous-server"); - if !s.is_empty() { - return vec![s]; - } - let s = PROD_RENDEZVOUS_SERVER.read().unwrap().clone(); - if !s.is_empty() { - return vec![s]; - } - let serial_obsolute = CONFIG2.read().unwrap().serial > SERIAL; - if serial_obsolute { - let ss: Vec = Self::get_option("rendezvous-servers") - .split(',') - .filter(|x| x.contains('.')) - .map(|x| x.to_owned()) - .collect(); - if !ss.is_empty() { - return ss; - } - } - return RENDEZVOUS_SERVERS.iter().map(|x| x.to_string()).collect(); - } - - pub fn reset_online() { - *ONLINE.lock().unwrap() = Default::default(); - } - - pub fn update_latency(host: &str, latency: i64) { - ONLINE.lock().unwrap().insert(host.to_owned(), latency); - let mut host = "".to_owned(); - let mut delay = i64::MAX; - for (tmp_host, tmp_delay) in ONLINE.lock().unwrap().iter() { - if tmp_delay > &0 && tmp_delay < &delay { - delay = *tmp_delay; - host = tmp_host.to_string(); - } - } - if !host.is_empty() { - let mut config = CONFIG2.write().unwrap(); - if host != config.rendezvous_server { - log::debug!("Update rendezvous_server in config to {}", host); - log::debug!("{:?}", *ONLINE.lock().unwrap()); - config.rendezvous_server = host; - config.store(); - } - } - } - - pub fn set_id(id: &str) { - let mut config = CONFIG.write().unwrap(); - if id == config.id { - return; - } - config.id = id.into(); - config.store(); - } - - pub fn set_nat_type(nat_type: i32) { - let mut config = CONFIG2.write().unwrap(); - if nat_type == config.nat_type { - return; - } - config.nat_type = nat_type; - config.store(); - } - - pub fn get_nat_type() -> i32 { - CONFIG2.read().unwrap().nat_type - } - - pub fn set_serial(serial: i32) { - let mut config = CONFIG2.write().unwrap(); - if serial == config.serial { - return; - } - config.serial = serial; - config.store(); - } - - pub fn get_serial() -> i32 { - std::cmp::max(CONFIG2.read().unwrap().serial, SERIAL) - } - - fn get_auto_id() -> Option { - #[cfg(any(target_os = "android", target_os = "ios"))] - { - return Some( - rand::thread_rng() - .gen_range(1_000_000_000..2_000_000_000) - .to_string(), - ); - } - - #[cfg(not(any(target_os = "android", target_os = "ios")))] - { - let mut id = 0u32; - if let Ok(Some(ma)) = mac_address::get_mac_address() { - for x in &ma.bytes()[2..] { - id = (id << 8) | (*x as u32); - } - id &= 0x1FFFFFFF; - Some(id.to_string()) - } else { - None - } - } - } - - pub fn get_auto_password(length: usize) -> String { - let mut rng = rand::thread_rng(); - (0..length) - .map(|_| CHARS[rng.gen::() % CHARS.len()]) - .collect() - } - - pub fn get_key_confirmed() -> bool { - CONFIG.read().unwrap().key_confirmed - } - - pub fn set_key_confirmed(v: bool) { - let mut config = CONFIG.write().unwrap(); - if config.key_confirmed == v { - return; - } - config.key_confirmed = v; - if !v { - config.keys_confirmed = Default::default(); - } - config.store(); - } - - pub fn get_host_key_confirmed(host: &str) -> bool { - matches!(CONFIG.read().unwrap().keys_confirmed.get(host), Some(true)) - } - - pub fn set_host_key_confirmed(host: &str, v: bool) { - if Self::get_host_key_confirmed(host) == v { - return; - } - let mut config = CONFIG.write().unwrap(); - config.keys_confirmed.insert(host.to_owned(), v); - config.store(); - } - - pub fn get_key_pair() -> KeyPair { - // lock here to make sure no gen_keypair more than once - // no use of CONFIG directly here to ensure no recursive calling in Config::load because of password dec which calling this function - let mut lock = KEY_PAIR.lock().unwrap(); - if let Some(p) = lock.as_ref() { - return p.clone(); - } - let mut config = Config::load_::(""); - if config.key_pair.0.is_empty() { - log::info!("Generated new keypair for id: {}", config.id); - let (pk, sk) = sign::gen_keypair(); - let key_pair = (sk.0.to_vec(), pk.0.into()); - config.key_pair = key_pair.clone(); - std::thread::spawn(|| { - let mut config = CONFIG.write().unwrap(); - config.key_pair = key_pair; - config.store(); - }); - } - *lock = Some(config.key_pair.clone()); - config.key_pair - } - - pub fn get_id() -> String { - let mut id = CONFIG.read().unwrap().id.clone(); - if id.is_empty() { - if let Some(tmp) = Config::get_auto_id() { - id = tmp; - Config::set_id(&id); - } - } - id - } - - pub fn get_id_or(b: String) -> String { - let a = CONFIG.read().unwrap().id.clone(); - if a.is_empty() { - b - } else { - a - } - } - - pub fn get_options() -> HashMap { - let mut res = DEFAULT_SETTINGS.read().unwrap().clone(); - res.extend(CONFIG2.read().unwrap().options.clone()); - res.extend(OVERWRITE_SETTINGS.read().unwrap().clone()); - res - } - - #[inline] - fn purify_options(v: &mut HashMap) { - v.retain(|k, v| is_option_can_save(&OVERWRITE_SETTINGS, k, &DEFAULT_SETTINGS, v)); - } - - pub fn set_options(mut v: HashMap) { - Self::purify_options(&mut v); - let mut config = CONFIG2.write().unwrap(); - if config.options == v { - return; - } - config.options = v; - config.store(); - } - - pub fn get_option(k: &str) -> String { - get_or( - &OVERWRITE_SETTINGS, - &CONFIG2.read().unwrap().options, - &DEFAULT_SETTINGS, - k, - ) - .unwrap_or_default() - } - - pub fn get_bool_option(k: &str) -> bool { - option2bool(k, &Self::get_option(k)) - } - - pub fn set_option(k: String, v: String) { - if !is_option_can_save(&OVERWRITE_SETTINGS, &k, &DEFAULT_SETTINGS, &v) { - return; - } - let mut config = CONFIG2.write().unwrap(); - let v2 = if v.is_empty() { None } else { Some(&v) }; - if v2 != config.options.get(&k) { - if v2.is_none() { - config.options.remove(&k); - } else { - config.options.insert(k, v); - } - config.store(); - } - } - - pub fn update_id() { - // to-do: how about if one ip register a lot of ids? - let id = Self::get_id(); - let mut rng = rand::thread_rng(); - let new_id = rng.gen_range(1_000_000_000..2_000_000_000).to_string(); - Config::set_id(&new_id); - log::info!("id updated from {} to {}", id, new_id); - } - - pub fn set_permanent_password(password: &str) { - if HARD_SETTINGS - .read() - .unwrap() - .get("password") - .map_or(false, |v| v == password) - { - return; - } - let mut config = CONFIG.write().unwrap(); - if password == config.password { - return; - } - config.password = password.into(); - config.store(); - Self::clear_trusted_devices(); - } - - pub fn get_permanent_password() -> String { - let mut password = CONFIG.read().unwrap().password.clone(); - if password.is_empty() { - if let Some(v) = HARD_SETTINGS.read().unwrap().get("password") { - password = v.to_owned(); - } - } - password - } - - pub fn set_salt(salt: &str) { - let mut config = CONFIG.write().unwrap(); - if salt == config.salt { - return; - } - config.salt = salt.into(); - config.store(); - } - - pub fn get_salt() -> String { - let mut salt = CONFIG.read().unwrap().salt.clone(); - if salt.is_empty() { - salt = Config::get_auto_password(6); - Config::set_salt(&salt); - } - salt - } - - pub fn set_socks(socks: Option) { - let mut config = CONFIG2.write().unwrap(); - if config.socks == socks { - return; - } - config.socks = socks; - config.store(); - } - - #[inline] - fn get_socks_from_custom_client_advanced_settings( - settings: &HashMap, - ) -> Option { - let url = settings.get(keys::OPTION_PROXY_URL)?; - Some(Socks5Server { - proxy: url.to_owned(), - username: settings - .get(keys::OPTION_PROXY_USERNAME) - .map(|x| x.to_string()) - .unwrap_or_default(), - password: settings - .get(keys::OPTION_PROXY_PASSWORD) - .map(|x| x.to_string()) - .unwrap_or_default(), - }) - } - - pub fn get_socks() -> Option { - Self::get_socks_from_custom_client_advanced_settings(&OVERWRITE_SETTINGS.read().unwrap()) - .or(CONFIG2.read().unwrap().socks.clone()) - .or(Self::get_socks_from_custom_client_advanced_settings( - &DEFAULT_SETTINGS.read().unwrap(), - )) - } - - #[inline] - pub fn is_proxy() -> bool { - Self::get_network_type() != NetworkType::Direct - } - - pub fn get_network_type() -> NetworkType { - if OVERWRITE_SETTINGS - .read() - .unwrap() - .get(keys::OPTION_PROXY_URL) - .is_some() - { - return NetworkType::ProxySocks; - } - if CONFIG2.read().unwrap().socks.is_some() { - return NetworkType::ProxySocks; - } - if DEFAULT_SETTINGS - .read() - .unwrap() - .get(keys::OPTION_PROXY_URL) - .is_some() - { - return NetworkType::ProxySocks; - } - NetworkType::Direct - } - - pub fn get_unlock_pin() -> String { - CONFIG2.read().unwrap().unlock_pin.clone() - } - - pub fn set_unlock_pin(pin: &str) { - let mut config = CONFIG2.write().unwrap(); - if pin == config.unlock_pin { - return; - } - config.unlock_pin = pin.to_string(); - config.store(); - } - - pub fn get_trusted_devices_json() -> String { - serde_json::to_string(&Self::get_trusted_devices()).unwrap_or_default() - } - - pub fn get_trusted_devices() -> Vec { - let (devices, synced) = TRUSTED_DEVICES.read().unwrap().clone(); - if synced { - return devices; - } - let devices = CONFIG2.read().unwrap().trusted_devices.clone(); - let (devices, succ, store) = decrypt_str_or_original(&devices, PASSWORD_ENC_VERSION); - if succ { - let mut devices: Vec = - serde_json::from_str(&devices).unwrap_or_default(); - let len = devices.len(); - devices.retain(|d| !d.outdate()); - if store || devices.len() != len { - Self::set_trusted_devices(devices.clone()); - } - *TRUSTED_DEVICES.write().unwrap() = (devices.clone(), true); - devices - } else { - Default::default() - } - } - - fn set_trusted_devices(mut trusted_devices: Vec) { - trusted_devices.retain(|d| !d.outdate()); - let devices = serde_json::to_string(&trusted_devices).unwrap_or_default(); - let max_len = 1024 * 1024; - if devices.bytes().len() > max_len { - log::error!("Trusted devices too large: {}", devices.bytes().len()); - return; - } - let devices = encrypt_str_or_original(&devices, PASSWORD_ENC_VERSION, max_len); - let mut config = CONFIG2.write().unwrap(); - config.trusted_devices = devices; - config.store(); - *TRUSTED_DEVICES.write().unwrap() = (trusted_devices, true); - } - - pub fn add_trusted_device(device: TrustedDevice) { - let mut devices = Self::get_trusted_devices(); - devices.retain(|d| d.hwid != device.hwid); - devices.push(device); - Self::set_trusted_devices(devices); - } - - pub fn remove_trusted_devices(hwids: &Vec) { - let mut devices = Self::get_trusted_devices(); - devices.retain(|d| !hwids.contains(&d.hwid)); - Self::set_trusted_devices(devices); - } - - pub fn clear_trusted_devices() { - Self::set_trusted_devices(Default::default()); - } - - pub fn get() -> Config { - return CONFIG.read().unwrap().clone(); - } - - pub fn set(cfg: Config) -> bool { - let mut lock = CONFIG.write().unwrap(); - if *lock == cfg { - return false; - } - *lock = cfg; - lock.store(); - true - } - - fn with_extension(path: PathBuf) -> PathBuf { - let ext = path.extension(); - if let Some(ext) = ext { - let ext = format!("{}.toml", ext.to_string_lossy()); - path.with_extension(ext) - } else { - path.with_extension("toml") - } - } -} - -const PEERS: &str = "peers"; - -impl PeerConfig { - pub fn load(id: &str) -> PeerConfig { - let _lock = CONFIG.read().unwrap(); - match confy::load_path(Self::path(id)) { - Ok(config) => { - let mut config: PeerConfig = config; - let mut store = false; - let (password, _, store2) = - decrypt_vec_or_original(&config.password, PASSWORD_ENC_VERSION); - config.password = password; - store = store || store2; - for opt in ["rdp_password", "os-username", "os-password"] { - if let Some(v) = config.options.get_mut(opt) { - let (encrypted, _, store2) = - decrypt_str_or_original(v, PASSWORD_ENC_VERSION); - *v = encrypted; - store = store || store2; - } - } - if store { - config.store(id); - } - config - } - Err(err) => { - if let confy::ConfyError::GeneralLoadError(err) = &err { - if err.kind() == std::io::ErrorKind::NotFound { - return Default::default(); - } - } - log::error!("Failed to load peer config '{}': {}", id, err); - Default::default() - } - } - } - - pub fn store(&self, id: &str) { - let _lock = CONFIG.read().unwrap(); - let mut config = self.clone(); - config.password = - encrypt_vec_or_original(&config.password, PASSWORD_ENC_VERSION, ENCRYPT_MAX_LEN); - for opt in ["rdp_password", "os-username", "os-password"] { - if let Some(v) = config.options.get_mut(opt) { - *v = encrypt_str_or_original(v, PASSWORD_ENC_VERSION, ENCRYPT_MAX_LEN) - } - } - if let Err(err) = store_path(Self::path(id), config) { - log::error!("Failed to store config: {}", err); - } - NEW_STORED_PEER_CONFIG.lock().unwrap().insert(id.to_owned()); - } - - pub fn remove(id: &str) { - fs::remove_file(Self::path(id)).ok(); - } - - fn path(id: &str) -> PathBuf { - //If the id contains invalid chars, encode it - let forbidden_paths = Regex::new(r".*[<>:/\\|\?\*].*"); - let path: PathBuf; - if let Ok(forbidden_paths) = forbidden_paths { - let id_encoded = if forbidden_paths.is_match(id) { - "base64_".to_string() + base64::encode(id, base64::Variant::Original).as_str() - } else { - id.to_string() - }; - path = [PEERS, id_encoded.as_str()].iter().collect(); - } else { - log::warn!("Regex create failed: {:?}", forbidden_paths.err()); - // fallback for failing to create this regex. - path = [PEERS, id.replace(":", "_").as_str()].iter().collect(); - } - Config::with_extension(Config::path(path)) - } - - pub fn peers(id_filters: Option>) -> Vec<(String, SystemTime, PeerConfig)> { - if let Ok(peers) = Config::path(PEERS).read_dir() { - if let Ok(peers) = peers - .map(|res| res.map(|e| e.path())) - .collect::, _>>() - { - let mut peers: Vec<_> = peers - .iter() - .filter(|p| { - p.is_file() - && p.extension().map(|p| p.to_str().unwrap_or("")) == Some("toml") - }) - .map(|p| { - let id = p - .file_stem() - .map(|p| p.to_str().unwrap_or("")) - .unwrap_or("") - .to_owned(); - - let id_decoded_string = if id.starts_with("base64_") && id.len() != 7 { - let id_decoded = base64::decode(&id[7..], base64::Variant::Original) - .unwrap_or_default(); - String::from_utf8_lossy(&id_decoded).as_ref().to_owned() - } else { - id - }; - (id_decoded_string, p) - }) - .filter(|(id, _)| { - let Some(filters) = &id_filters else { - return true; - }; - filters.contains(id) - }) - .map(|(id, p)| { - let t = crate::get_modified_time(p); - let c = PeerConfig::load(&id); - if c.info.platform.is_empty() { - fs::remove_file(p).ok(); - } - (id, t, c) - }) - .filter(|p| !p.2.info.platform.is_empty()) - .collect(); - peers.sort_unstable_by(|a, b| b.1.cmp(&a.1)); - return peers; - } - } - Default::default() - } - - pub fn exists(id: &str) -> bool { - Self::path(id).exists() - } - - serde_field_string!( - default_view_style, - deserialize_view_style, - UserDefaultConfig::read(keys::OPTION_VIEW_STYLE) - ); - serde_field_string!( - default_scroll_style, - deserialize_scroll_style, - UserDefaultConfig::read(keys::OPTION_SCROLL_STYLE) - ); - serde_field_string!( - default_image_quality, - deserialize_image_quality, - UserDefaultConfig::read(keys::OPTION_IMAGE_QUALITY) - ); - serde_field_string!( - default_reverse_mouse_wheel, - deserialize_reverse_mouse_wheel, - UserDefaultConfig::read(keys::OPTION_REVERSE_MOUSE_WHEEL) - ); - serde_field_string!( - default_displays_as_individual_windows, - deserialize_displays_as_individual_windows, - UserDefaultConfig::read(keys::OPTION_DISPLAYS_AS_INDIVIDUAL_WINDOWS) - ); - serde_field_string!( - default_use_all_my_displays_for_the_remote_session, - deserialize_use_all_my_displays_for_the_remote_session, - UserDefaultConfig::read(keys::OPTION_USE_ALL_MY_DISPLAYS_FOR_THE_REMOTE_SESSION) - ); - - fn default_custom_image_quality() -> Vec { - let f: f64 = UserDefaultConfig::read(keys::OPTION_CUSTOM_IMAGE_QUALITY) - .parse() - .unwrap_or(50.0); - vec![f as _] - } - - fn deserialize_custom_image_quality<'de, D>(deserializer: D) -> Result, D::Error> - where - D: de::Deserializer<'de>, - { - let v: Vec = de::Deserialize::deserialize(deserializer)?; - if v.len() == 1 && v[0] >= 10 && v[0] <= 0xFFF { - Ok(v) - } else { - Ok(Self::default_custom_image_quality()) - } - } - - fn default_options() -> HashMap { - let mut mp: HashMap = Default::default(); - [ - keys::OPTION_CODEC_PREFERENCE, - keys::OPTION_CUSTOM_FPS, - keys::OPTION_ZOOM_CURSOR, - keys::OPTION_TOUCH_MODE, - keys::OPTION_I444, - keys::OPTION_SWAP_LEFT_RIGHT_MOUSE, - keys::OPTION_COLLAPSE_TOOLBAR, - ] - .map(|key| { - mp.insert(key.to_owned(), UserDefaultConfig::read(key)); - }); - mp - } -} - -serde_field_bool!( - ShowRemoteCursor, - "show_remote_cursor", - default_show_remote_cursor, - "ShowRemoteCursor::default_show_remote_cursor" -); -serde_field_bool!( - FollowRemoteCursor, - "follow_remote_cursor", - default_follow_remote_cursor, - "FollowRemoteCursor::default_follow_remote_cursor" -); - -serde_field_bool!( - FollowRemoteWindow, - "follow_remote_window", - default_follow_remote_window, - "FollowRemoteWindow::default_follow_remote_window" -); -serde_field_bool!( - ShowQualityMonitor, - "show_quality_monitor", - default_show_quality_monitor, - "ShowQualityMonitor::default_show_quality_monitor" -); -serde_field_bool!( - DisableAudio, - "disable_audio", - default_disable_audio, - "DisableAudio::default_disable_audio" -); -serde_field_bool!( - EnableFileCopyPaste, - "enable-file-copy-paste", - default_enable_file_copy_paste, - "EnableFileCopyPaste::default_enable_file_copy_paste" -); -serde_field_bool!( - DisableClipboard, - "disable_clipboard", - default_disable_clipboard, - "DisableClipboard::default_disable_clipboard" -); -serde_field_bool!( - LockAfterSessionEnd, - "lock_after_session_end", - default_lock_after_session_end, - "LockAfterSessionEnd::default_lock_after_session_end" -); -serde_field_bool!( - PrivacyMode, - "privacy_mode", - default_privacy_mode, - "PrivacyMode::default_privacy_mode" -); - -serde_field_bool!( - AllowSwapKey, - "allow_swap_key", - default_allow_swap_key, - "AllowSwapKey::default_allow_swap_key" -); - -serde_field_bool!( - ViewOnly, - "view_only", - default_view_only, - "ViewOnly::default_view_only" -); - -serde_field_bool!( - SyncInitClipboard, - "sync-init-clipboard", - default_sync_init_clipboard, - "SyncInitClipboard::default_sync_init_clipboard" -); - -#[derive(Debug, Default, Serialize, Deserialize, Clone)] -pub struct LocalConfig { - #[serde(default, deserialize_with = "deserialize_string")] - remote_id: String, // latest used one - #[serde(default, deserialize_with = "deserialize_string")] - kb_layout_type: String, - #[serde(default, deserialize_with = "deserialize_size")] - size: Size, - #[serde(default, deserialize_with = "deserialize_vec_string")] - pub fav: Vec, - #[serde(default, deserialize_with = "deserialize_hashmap_string_string")] - options: HashMap, - // Various data for flutter ui - #[serde(default, deserialize_with = "deserialize_hashmap_string_string")] - ui_flutter: HashMap, -} - -impl LocalConfig { - fn load() -> LocalConfig { - Config::load_::("_local") - } - - fn store(&self) { - Config::store_(self, "_local"); - } - - pub fn get_kb_layout_type() -> String { - LOCAL_CONFIG.read().unwrap().kb_layout_type.clone() - } - - pub fn set_kb_layout_type(kb_layout_type: String) { - let mut config = LOCAL_CONFIG.write().unwrap(); - config.kb_layout_type = kb_layout_type; - config.store(); - } - - pub fn get_size() -> Size { - LOCAL_CONFIG.read().unwrap().size - } - - pub fn set_size(x: i32, y: i32, w: i32, h: i32) { - let mut config = LOCAL_CONFIG.write().unwrap(); - let size = (x, y, w, h); - if size == config.size || size.2 < 300 || size.3 < 300 { - return; - } - config.size = size; - config.store(); - } - - pub fn set_remote_id(remote_id: &str) { - let mut config = LOCAL_CONFIG.write().unwrap(); - if remote_id == config.remote_id { - return; - } - config.remote_id = remote_id.into(); - config.store(); - } - - pub fn get_remote_id() -> String { - LOCAL_CONFIG.read().unwrap().remote_id.clone() - } - - pub fn set_fav(fav: Vec) { - let mut lock = LOCAL_CONFIG.write().unwrap(); - if lock.fav == fav { - return; - } - lock.fav = fav; - lock.store(); - } - - pub fn get_fav() -> Vec { - LOCAL_CONFIG.read().unwrap().fav.clone() - } - - pub fn get_option(k: &str) -> String { - get_or( - &OVERWRITE_LOCAL_SETTINGS, - &LOCAL_CONFIG.read().unwrap().options, - &DEFAULT_LOCAL_SETTINGS, - k, - ) - .unwrap_or_default() - } - - // Usually get_option should be used. - pub fn get_option_from_file(k: &str) -> String { - get_or( - &OVERWRITE_LOCAL_SETTINGS, - &Self::load().options, - &DEFAULT_LOCAL_SETTINGS, - k, - ) - .unwrap_or_default() - } - - pub fn get_bool_option(k: &str) -> bool { - option2bool(k, &Self::get_option(k)) - } - - pub fn set_option(k: String, v: String) { - if !is_option_can_save(&OVERWRITE_LOCAL_SETTINGS, &k, &DEFAULT_LOCAL_SETTINGS, &v) { - return; - } - let mut config = LOCAL_CONFIG.write().unwrap(); - // The custom client will explictly set "default" as the default language. - let is_custom_client_default_lang = k == keys::OPTION_LANGUAGE && v == "default"; - if is_custom_client_default_lang { - config.options.insert(k, "".to_owned()); - config.store(); - return; - } - let v2 = if v.is_empty() { None } else { Some(&v) }; - if v2 != config.options.get(&k) { - if v2.is_none() { - config.options.remove(&k); - } else { - config.options.insert(k, v); - } - config.store(); - } - } - - pub fn get_flutter_option(k: &str) -> String { - get_or( - &OVERWRITE_LOCAL_SETTINGS, - &LOCAL_CONFIG.read().unwrap().ui_flutter, - &DEFAULT_LOCAL_SETTINGS, - k, - ) - .unwrap_or_default() - } - - pub fn set_flutter_option(k: String, v: String) { - let mut config = LOCAL_CONFIG.write().unwrap(); - let v2 = if v.is_empty() { None } else { Some(&v) }; - if v2 != config.ui_flutter.get(&k) { - if v2.is_none() { - config.ui_flutter.remove(&k); - } else { - config.ui_flutter.insert(k, v); - } - config.store(); - } - } -} - -#[derive(Debug, Default, Serialize, Deserialize, Clone)] -pub struct DiscoveryPeer { - #[serde(default, deserialize_with = "deserialize_string")] - pub id: String, - #[serde(default, deserialize_with = "deserialize_string")] - pub username: String, - #[serde(default, deserialize_with = "deserialize_string")] - pub hostname: String, - #[serde(default, deserialize_with = "deserialize_string")] - pub platform: String, - #[serde(default, deserialize_with = "deserialize_bool")] - pub online: bool, - #[serde(default, deserialize_with = "deserialize_hashmap_string_string")] - pub ip_mac: HashMap, -} - -impl DiscoveryPeer { - pub fn is_same_peer(&self, other: &DiscoveryPeer) -> bool { - self.id == other.id && self.username == other.username - } -} - -#[derive(Debug, Default, Serialize, Deserialize, Clone)] -pub struct LanPeers { - #[serde(default, deserialize_with = "deserialize_vec_discoverypeer")] - pub peers: Vec, -} - -impl LanPeers { - pub fn load() -> LanPeers { - let _lock = CONFIG.read().unwrap(); - match confy::load_path(Config::file_("_lan_peers")) { - Ok(peers) => peers, - Err(err) => { - log::error!("Failed to load lan peers: {}", err); - Default::default() - } - } - } - - pub fn store(peers: &[DiscoveryPeer]) { - let f = LanPeers { - peers: peers.to_owned(), - }; - if let Err(err) = store_path(Config::file_("_lan_peers"), f) { - log::error!("Failed to store lan peers: {}", err); - } - } - - pub fn modify_time() -> crate::ResultType { - let p = Config::file_("_lan_peers"); - Ok(fs::metadata(p)? - .modified()? - .duration_since(SystemTime::UNIX_EPOCH)? - .as_millis() as _) - } -} - -#[derive(Debug, Default, Serialize, Deserialize, Clone)] -pub struct UserDefaultConfig { - #[serde(default, deserialize_with = "deserialize_hashmap_string_string")] - options: HashMap, -} - -impl UserDefaultConfig { - fn read(key: &str) -> String { - let mut cfg = USER_DEFAULT_CONFIG.write().unwrap(); - // we do so, because default config may changed in another process, but we don't sync it - // but no need to read every time, give a small interval to avoid too many redundant read waste - if cfg.1.elapsed() > Duration::from_secs(1) { - *cfg = (Self::load(), Instant::now()); - } - cfg.0.get(key) - } - - pub fn load() -> UserDefaultConfig { - Config::load_::("_default") - } - - #[inline] - fn store(&self) { - Config::store_(self, "_default"); - } - - pub fn get(&self, key: &str) -> String { - match key { - #[cfg(any(target_os = "android", target_os = "ios"))] - keys::OPTION_VIEW_STYLE => self.get_string(key, "adaptive", vec!["original"]), - #[cfg(not(any(target_os = "android", target_os = "ios")))] - keys::OPTION_VIEW_STYLE => self.get_string(key, "original", vec!["adaptive"]), - keys::OPTION_SCROLL_STYLE => self.get_string(key, "scrollauto", vec!["scrollbar"]), - keys::OPTION_IMAGE_QUALITY => { - self.get_string(key, "balanced", vec!["best", "low", "custom"]) - } - keys::OPTION_CODEC_PREFERENCE => { - self.get_string(key, "auto", vec!["vp8", "vp9", "av1", "h264", "h265"]) - } - keys::OPTION_CUSTOM_IMAGE_QUALITY => { - self.get_double_string(key, 50.0, 10.0, 0xFFF as f64) - } - keys::OPTION_CUSTOM_FPS => self.get_double_string(key, 30.0, 5.0, 120.0), - keys::OPTION_ENABLE_FILE_COPY_PASTE => self.get_string(key, "Y", vec!["", "N"]), - _ => self - .get_after(key) - .map(|v| v.to_string()) - .unwrap_or_default(), - } - } - - pub fn set(&mut self, key: String, value: String) { - if !is_option_can_save( - &OVERWRITE_DISPLAY_SETTINGS, - &key, - &DEFAULT_DISPLAY_SETTINGS, - &value, - ) { - return; - } - if value.is_empty() { - self.options.remove(&key); - } else { - self.options.insert(key, value); - } - self.store(); - } - - #[inline] - fn get_string(&self, key: &str, default: &str, others: Vec<&str>) -> String { - match self.get_after(key) { - Some(option) => { - if others.contains(&option.as_str()) { - option.to_owned() - } else { - default.to_owned() - } - } - None => default.to_owned(), - } - } - - #[inline] - fn get_double_string(&self, key: &str, default: f64, min: f64, max: f64) -> String { - match self.get_after(key) { - Some(option) => { - let v: f64 = option.parse().unwrap_or(default); - if v >= min && v <= max { - v.to_string() - } else { - default.to_string() - } - } - None => default.to_string(), - } - } - - fn get_after(&self, k: &str) -> Option { - get_or( - &OVERWRITE_DISPLAY_SETTINGS, - &self.options, - &DEFAULT_DISPLAY_SETTINGS, - k, - ) - } -} - -#[derive(Debug, Default, Serialize, Deserialize, Clone)] -pub struct AbPeer { - #[serde( - default, - deserialize_with = "deserialize_string", - skip_serializing_if = "String::is_empty" - )] - pub id: String, - #[serde( - default, - deserialize_with = "deserialize_string", - skip_serializing_if = "String::is_empty" - )] - pub hash: String, - #[serde( - default, - deserialize_with = "deserialize_string", - skip_serializing_if = "String::is_empty" - )] - pub username: String, - #[serde( - default, - deserialize_with = "deserialize_string", - skip_serializing_if = "String::is_empty" - )] - pub hostname: String, - #[serde( - default, - deserialize_with = "deserialize_string", - skip_serializing_if = "String::is_empty" - )] - pub platform: String, - #[serde( - default, - deserialize_with = "deserialize_string", - skip_serializing_if = "String::is_empty" - )] - pub alias: String, - #[serde(default, deserialize_with = "deserialize_vec_string")] - pub tags: Vec, -} - -#[derive(Debug, Default, Serialize, Deserialize, Clone)] -pub struct AbEntry { - #[serde( - default, - deserialize_with = "deserialize_string", - skip_serializing_if = "String::is_empty" - )] - pub guid: String, - #[serde( - default, - deserialize_with = "deserialize_string", - skip_serializing_if = "String::is_empty" - )] - pub name: String, - #[serde(default, deserialize_with = "deserialize_vec_abpeer")] - pub peers: Vec, - #[serde(default, deserialize_with = "deserialize_vec_string")] - pub tags: Vec, - #[serde( - default, - deserialize_with = "deserialize_string", - skip_serializing_if = "String::is_empty" - )] - pub tag_colors: String, -} - -impl AbEntry { - pub fn personal(&self) -> bool { - self.name == "My address book" || self.name == "Legacy address book" - } -} - -#[derive(Debug, Default, Serialize, Deserialize, Clone)] -pub struct Ab { - #[serde( - default, - deserialize_with = "deserialize_string", - skip_serializing_if = "String::is_empty" - )] - pub access_token: String, - #[serde(default, deserialize_with = "deserialize_vec_abentry")] - pub ab_entries: Vec, -} - -impl Ab { - fn path() -> PathBuf { - let filename = format!("{}_ab", APP_NAME.read().unwrap().clone()); - Config::path(filename) - } - - pub fn store(json: String) { - if let Ok(mut file) = std::fs::File::create(Self::path()) { - let data = compress(json.as_bytes()); - let max_len = 64 * 1024 * 1024; - if data.len() > max_len { - // maxlen of function decompress - log::error!("ab data too large, {} > {}", data.len(), max_len); - return; - } - if let Ok(data) = symmetric_crypt(&data, true) { - file.write_all(&data).ok(); - } - }; - } - - pub fn load() -> Ab { - if let Ok(mut file) = std::fs::File::open(Self::path()) { - let mut data = vec![]; - if file.read_to_end(&mut data).is_ok() { - if let Ok(data) = symmetric_crypt(&data, false) { - let data = decompress(&data); - if let Ok(ab) = serde_json::from_str::(&String::from_utf8_lossy(&data)) { - return ab; - } - } - } - }; - Self::remove(); - Ab::default() - } - - pub fn remove() { - std::fs::remove_file(Self::path()).ok(); - } -} - -// use default value when field type is wrong -macro_rules! deserialize_default { - ($func_name:ident, $return_type:ty) => { - fn $func_name<'de, D>(deserializer: D) -> Result<$return_type, D::Error> - where - D: de::Deserializer<'de>, - { - Ok(de::Deserialize::deserialize(deserializer).unwrap_or_default()) - } - }; -} - -#[derive(Debug, Default, Serialize, Deserialize, Clone)] -pub struct GroupPeer { - #[serde( - default, - deserialize_with = "deserialize_string", - skip_serializing_if = "String::is_empty" - )] - pub id: String, - #[serde( - default, - deserialize_with = "deserialize_string", - skip_serializing_if = "String::is_empty" - )] - pub username: String, - #[serde( - default, - deserialize_with = "deserialize_string", - skip_serializing_if = "String::is_empty" - )] - pub hostname: String, - #[serde( - default, - deserialize_with = "deserialize_string", - skip_serializing_if = "String::is_empty" - )] - pub platform: String, - #[serde( - default, - deserialize_with = "deserialize_string", - skip_serializing_if = "String::is_empty" - )] - pub login_name: String, -} - -#[derive(Debug, Default, Serialize, Deserialize, Clone)] -pub struct GroupUser { - #[serde( - default, - deserialize_with = "deserialize_string", - skip_serializing_if = "String::is_empty" - )] - pub name: String, -} - -#[derive(Debug, Default, Serialize, Deserialize, Clone)] -pub struct Group { - #[serde( - default, - deserialize_with = "deserialize_string", - skip_serializing_if = "String::is_empty" - )] - pub access_token: String, - #[serde(default, deserialize_with = "deserialize_vec_groupuser")] - pub users: Vec, - #[serde(default, deserialize_with = "deserialize_vec_grouppeer")] - pub peers: Vec, -} - -impl Group { - fn path() -> PathBuf { - let filename = format!("{}_group", APP_NAME.read().unwrap().clone()); - Config::path(filename) - } - - pub fn store(json: String) { - if let Ok(mut file) = std::fs::File::create(Self::path()) { - let data = compress(json.as_bytes()); - let max_len = 64 * 1024 * 1024; - if data.len() > max_len { - // maxlen of function decompress - return; - } - if let Ok(data) = symmetric_crypt(&data, true) { - file.write_all(&data).ok(); - } - }; - } - - pub fn load() -> Self { - if let Ok(mut file) = std::fs::File::open(Self::path()) { - let mut data = vec![]; - if file.read_to_end(&mut data).is_ok() { - if let Ok(data) = symmetric_crypt(&data, false) { - let data = decompress(&data); - if let Ok(group) = serde_json::from_str::(&String::from_utf8_lossy(&data)) - { - return group; - } - } - } - }; - Self::remove(); - Self::default() - } - - pub fn remove() { - std::fs::remove_file(Self::path()).ok(); - } -} - -#[derive(Debug, Default, Serialize, Deserialize, Clone)] -pub struct TrustedDevice { - pub hwid: Bytes, - pub time: i64, - pub id: String, - pub name: String, - pub platform: String, -} - -impl TrustedDevice { - pub fn outdate(&self) -> bool { - const DAYS_90: i64 = 90 * 24 * 60 * 60 * 1000; - self.time + DAYS_90 < crate::get_time() - } -} - -deserialize_default!(deserialize_string, String); -deserialize_default!(deserialize_bool, bool); -deserialize_default!(deserialize_i32, i32); -deserialize_default!(deserialize_vec_u8, Vec); -deserialize_default!(deserialize_vec_string, Vec); -deserialize_default!(deserialize_vec_i32_string_i32, Vec<(i32, String, i32)>); -deserialize_default!(deserialize_vec_discoverypeer, Vec); -deserialize_default!(deserialize_vec_abpeer, Vec); -deserialize_default!(deserialize_vec_abentry, Vec); -deserialize_default!(deserialize_vec_groupuser, Vec); -deserialize_default!(deserialize_vec_grouppeer, Vec); -deserialize_default!(deserialize_keypair, KeyPair); -deserialize_default!(deserialize_size, Size); -deserialize_default!(deserialize_hashmap_string_string, HashMap); -deserialize_default!(deserialize_hashmap_string_bool, HashMap); -deserialize_default!(deserialize_hashmap_resolutions, HashMap); - -#[inline] -fn get_or( - a: &RwLock>, - b: &HashMap, - c: &RwLock>, - k: &str, -) -> Option { - a.read() - .unwrap() - .get(k) - .or(b.get(k)) - .or(c.read().unwrap().get(k)) - .cloned() -} - -#[inline] -fn is_option_can_save( - overwrite: &RwLock>, - k: &str, - defaults: &RwLock>, - v: &str, -) -> bool { - if overwrite.read().unwrap().contains_key(k) - || defaults.read().unwrap().get(k).map_or(false, |x| x == v) - { - return false; - } - true -} - -#[inline] -pub fn is_incoming_only() -> bool { - HARD_SETTINGS - .read() - .unwrap() - .get("conn-type") - .map_or(false, |x| x == ("incoming")) -} - -#[inline] -pub fn is_outgoing_only() -> bool { - HARD_SETTINGS - .read() - .unwrap() - .get("conn-type") - .map_or(false, |x| x == ("outgoing")) -} - -#[inline] -fn is_some_hard_opton(name: &str) -> bool { - HARD_SETTINGS - .read() - .unwrap() - .get(name) - .map_or(false, |x| x == ("Y")) -} - -#[inline] -pub fn is_disable_tcp_listen() -> bool { - is_some_hard_opton("disable-tcp-listen") -} - -#[inline] -pub fn is_disable_settings() -> bool { - is_some_hard_opton("disable-settings") -} - -#[inline] -pub fn is_disable_ab() -> bool { - is_some_hard_opton("disable-ab") -} - -#[inline] -pub fn is_disable_account() -> bool { - is_some_hard_opton("disable-account") -} - -#[inline] -pub fn is_disable_installation() -> bool { - is_some_hard_opton("disable-installation") -} - -// This function must be kept the same as the one in flutter and sciter code. -// flutter: flutter/lib/common.dart -> option2bool() -// sciter: Does not have the function, but it should be kept the same. -pub fn option2bool(option: &str, value: &str) -> bool { - if option.starts_with("enable-") { - value != "N" - } else if option.starts_with("allow-") - || option == "stop-service" - || option == keys::OPTION_DIRECT_SERVER - || option == "force-always-relay" - { - value == "Y" - } else { - value != "N" - } -} - -pub mod keys { - pub const OPTION_VIEW_ONLY: &str = "view_only"; - pub const OPTION_SHOW_MONITORS_TOOLBAR: &str = "show_monitors_toolbar"; - pub const OPTION_COLLAPSE_TOOLBAR: &str = "collapse_toolbar"; - pub const OPTION_SHOW_REMOTE_CURSOR: &str = "show_remote_cursor"; - pub const OPTION_FOLLOW_REMOTE_CURSOR: &str = "follow_remote_cursor"; - pub const OPTION_FOLLOW_REMOTE_WINDOW: &str = "follow_remote_window"; - pub const OPTION_ZOOM_CURSOR: &str = "zoom-cursor"; - pub const OPTION_SHOW_QUALITY_MONITOR: &str = "show_quality_monitor"; - pub const OPTION_DISABLE_AUDIO: &str = "disable_audio"; - pub const OPTION_ENABLE_FILE_COPY_PASTE: &str = "enable-file-copy-paste"; - pub const OPTION_DISABLE_CLIPBOARD: &str = "disable_clipboard"; - pub const OPTION_LOCK_AFTER_SESSION_END: &str = "lock_after_session_end"; - pub const OPTION_PRIVACY_MODE: &str = "privacy_mode"; - pub const OPTION_TOUCH_MODE: &str = "touch-mode"; - pub const OPTION_I444: &str = "i444"; - pub const OPTION_REVERSE_MOUSE_WHEEL: &str = "reverse_mouse_wheel"; - pub const OPTION_SWAP_LEFT_RIGHT_MOUSE: &str = "swap-left-right-mouse"; - pub const OPTION_DISPLAYS_AS_INDIVIDUAL_WINDOWS: &str = "displays_as_individual_windows"; - pub const OPTION_USE_ALL_MY_DISPLAYS_FOR_THE_REMOTE_SESSION: &str = - "use_all_my_displays_for_the_remote_session"; - pub const OPTION_VIEW_STYLE: &str = "view_style"; - pub const OPTION_SCROLL_STYLE: &str = "scroll_style"; - pub const OPTION_IMAGE_QUALITY: &str = "image_quality"; - pub const OPTION_CUSTOM_IMAGE_QUALITY: &str = "custom_image_quality"; - pub const OPTION_CUSTOM_FPS: &str = "custom-fps"; - pub const OPTION_CODEC_PREFERENCE: &str = "codec-preference"; - pub const OPTION_SYNC_INIT_CLIPBOARD: &str = "sync-init-clipboard"; - pub const OPTION_THEME: &str = "theme"; - pub const OPTION_LANGUAGE: &str = "lang"; - pub const OPTION_REMOTE_MENUBAR_DRAG_LEFT: &str = "remote-menubar-drag-left"; - pub const OPTION_REMOTE_MENUBAR_DRAG_RIGHT: &str = "remote-menubar-drag-right"; - pub const OPTION_HIDE_AB_TAGS_PANEL: &str = "hideAbTagsPanel"; - pub const OPTION_ENABLE_CONFIRM_CLOSING_TABS: &str = "enable-confirm-closing-tabs"; - pub const OPTION_ENABLE_OPEN_NEW_CONNECTIONS_IN_TABS: &str = - "enable-open-new-connections-in-tabs"; - pub const OPTION_TEXTURE_RENDER: &str = "use-texture-render"; - pub const OPTION_ENABLE_CHECK_UPDATE: &str = "enable-check-update"; - pub const OPTION_SYNC_AB_WITH_RECENT_SESSIONS: &str = "sync-ab-with-recent-sessions"; - pub const OPTION_SYNC_AB_TAGS: &str = "sync-ab-tags"; - pub const OPTION_FILTER_AB_BY_INTERSECTION: &str = "filter-ab-by-intersection"; - pub const OPTION_ACCESS_MODE: &str = "access-mode"; - pub const OPTION_ENABLE_KEYBOARD: &str = "enable-keyboard"; - pub const OPTION_ENABLE_CLIPBOARD: &str = "enable-clipboard"; - pub const OPTION_ENABLE_FILE_TRANSFER: &str = "enable-file-transfer"; - pub const OPTION_ENABLE_AUDIO: &str = "enable-audio"; - pub const OPTION_ENABLE_TUNNEL: &str = "enable-tunnel"; - pub const OPTION_ENABLE_REMOTE_RESTART: &str = "enable-remote-restart"; - pub const OPTION_ENABLE_RECORD_SESSION: &str = "enable-record-session"; - pub const OPTION_ENABLE_BLOCK_INPUT: &str = "enable-block-input"; - pub const OPTION_ALLOW_REMOTE_CONFIG_MODIFICATION: &str = "allow-remote-config-modification"; - pub const OPTION_ENABLE_LAN_DISCOVERY: &str = "enable-lan-discovery"; - pub const OPTION_DIRECT_SERVER: &str = "direct-server"; - pub const OPTION_DIRECT_ACCESS_PORT: &str = "direct-access-port"; - pub const OPTION_WHITELIST: &str = "whitelist"; - pub const OPTION_ALLOW_AUTO_DISCONNECT: &str = "allow-auto-disconnect"; - pub const OPTION_AUTO_DISCONNECT_TIMEOUT: &str = "auto-disconnect-timeout"; - pub const OPTION_ALLOW_ONLY_CONN_WINDOW_OPEN: &str = "allow-only-conn-window-open"; - pub const OPTION_ALLOW_AUTO_RECORD_INCOMING: &str = "allow-auto-record-incoming"; - pub const OPTION_ALLOW_AUTO_RECORD_OUTGOING: &str = "allow-auto-record-outgoing"; - pub const OPTION_VIDEO_SAVE_DIRECTORY: &str = "video-save-directory"; - pub const OPTION_ENABLE_ABR: &str = "enable-abr"; - pub const OPTION_ALLOW_REMOVE_WALLPAPER: &str = "allow-remove-wallpaper"; - pub const OPTION_ALLOW_ALWAYS_SOFTWARE_RENDER: &str = "allow-always-software-render"; - pub const OPTION_ALLOW_LINUX_HEADLESS: &str = "allow-linux-headless"; - pub const OPTION_ENABLE_HWCODEC: &str = "enable-hwcodec"; - pub const OPTION_APPROVE_MODE: &str = "approve-mode"; - pub const OPTION_VERIFICATION_METHOD: &str = "verification-method"; - pub const OPTION_CUSTOM_RENDEZVOUS_SERVER: &str = "custom-rendezvous-server"; - pub const OPTION_API_SERVER: &str = "api-server"; - pub const OPTION_KEY: &str = "key"; - pub const OPTION_PRESET_ADDRESS_BOOK_NAME: &str = "preset-address-book-name"; - pub const OPTION_PRESET_ADDRESS_BOOK_TAG: &str = "preset-address-book-tag"; - pub const OPTION_ENABLE_DIRECTX_CAPTURE: &str = "enable-directx-capture"; - pub const OPTION_ENABLE_ANDROID_SOFTWARE_ENCODING_HALF_SCALE: &str = - "enable-android-software-encoding-half-scale"; - pub const OPTION_ENABLE_TRUSTED_DEVICES: &str = "enable-trusted-devices"; - pub const OPTION_AV1_TEST: &str = "av1-test"; - - // buildin options - pub const OPTION_DISPLAY_NAME: &str = "display-name"; - pub const OPTION_DISABLE_UDP: &str = "disable-udp"; - pub const OPTION_PRESET_USERNAME: &str = "preset-user-name"; - pub const OPTION_PRESET_STRATEGY_NAME: &str = "preset-strategy-name"; - pub const OPTION_REMOVE_PRESET_PASSWORD_WARNING: &str = "remove-preset-password-warning"; - pub const OPTION_HIDE_SECURITY_SETTINGS: &str = "hide-security-settings"; - pub const OPTION_HIDE_NETWORK_SETTINGS: &str = "hide-network-settings"; - pub const OPTION_HIDE_SERVER_SETTINGS: &str = "hide-server-settings"; - pub const OPTION_HIDE_PROXY_SETTINGS: &str = "hide-proxy-settings"; - pub const OPTION_HIDE_USERNAME_ON_CARD: &str = "hide-username-on-card"; - pub const OPTION_HIDE_HELP_CARDS: &str = "hide-help-cards"; - pub const OPTION_DEFAULT_CONNECT_PASSWORD: &str = "default-connect-password"; - pub const OPTION_HIDE_TRAY: &str = "hide-tray"; - pub const OPTION_ONE_WAY_CLIPBOARD_REDIRECTION: &str = "one-way-clipboard-redirection"; - pub const OPTION_ALLOW_LOGON_SCREEN_PASSWORD: &str = "allow-logon-screen-password"; - pub const OPTION_ONE_WAY_FILE_TRANSFER: &str = "one-way-file-transfer"; - - // flutter local options - pub const OPTION_FLUTTER_REMOTE_MENUBAR_STATE: &str = "remoteMenubarState"; - pub const OPTION_FLUTTER_PEER_SORTING: &str = "peer-sorting"; - pub const OPTION_FLUTTER_PEER_TAB_INDEX: &str = "peer-tab-index"; - pub const OPTION_FLUTTER_PEER_TAB_ORDER: &str = "peer-tab-order"; - pub const OPTION_FLUTTER_PEER_TAB_VISIBLE: &str = "peer-tab-visible"; - pub const OPTION_FLUTTER_PEER_CARD_UI_TYLE: &str = "peer-card-ui-type"; - pub const OPTION_FLUTTER_CURRENT_AB_NAME: &str = "current-ab-name"; - pub const OPTION_ALLOW_REMOTE_CM_MODIFICATION: &str = "allow-remote-cm-modification"; - - // android floating window options - pub const OPTION_DISABLE_FLOATING_WINDOW: &str = "disable-floating-window"; - pub const OPTION_FLOATING_WINDOW_SIZE: &str = "floating-window-size"; - pub const OPTION_FLOATING_WINDOW_UNTOUCHABLE: &str = "floating-window-untouchable"; - pub const OPTION_FLOATING_WINDOW_TRANSPARENCY: &str = "floating-window-transparency"; - pub const OPTION_FLOATING_WINDOW_SVG: &str = "floating-window-svg"; - - // android keep screen on - pub const OPTION_KEEP_SCREEN_ON: &str = "keep-screen-on"; - - pub const OPTION_DISABLE_GROUP_PANEL: &str = "disable-group-panel"; - pub const OPTION_PRE_ELEVATE_SERVICE: &str = "pre-elevate-service"; - - // proxy settings - // The following options are not real keys, they are just used for custom client advanced settings. - // The real keys are in Config2::socks. - pub const OPTION_PROXY_URL: &str = "proxy-url"; - pub const OPTION_PROXY_USERNAME: &str = "proxy-username"; - pub const OPTION_PROXY_PASSWORD: &str = "proxy-password"; - - // DEFAULT_DISPLAY_SETTINGS, OVERWRITE_DISPLAY_SETTINGS - pub const KEYS_DISPLAY_SETTINGS: &[&str] = &[ - OPTION_VIEW_ONLY, - OPTION_SHOW_MONITORS_TOOLBAR, - OPTION_COLLAPSE_TOOLBAR, - OPTION_SHOW_REMOTE_CURSOR, - OPTION_FOLLOW_REMOTE_CURSOR, - OPTION_FOLLOW_REMOTE_WINDOW, - OPTION_ZOOM_CURSOR, - OPTION_SHOW_QUALITY_MONITOR, - OPTION_DISABLE_AUDIO, - OPTION_ENABLE_FILE_COPY_PASTE, - OPTION_DISABLE_CLIPBOARD, - OPTION_LOCK_AFTER_SESSION_END, - OPTION_PRIVACY_MODE, - OPTION_TOUCH_MODE, - OPTION_I444, - OPTION_REVERSE_MOUSE_WHEEL, - OPTION_SWAP_LEFT_RIGHT_MOUSE, - OPTION_DISPLAYS_AS_INDIVIDUAL_WINDOWS, - OPTION_USE_ALL_MY_DISPLAYS_FOR_THE_REMOTE_SESSION, - OPTION_VIEW_STYLE, - OPTION_SCROLL_STYLE, - OPTION_IMAGE_QUALITY, - OPTION_CUSTOM_IMAGE_QUALITY, - OPTION_CUSTOM_FPS, - OPTION_CODEC_PREFERENCE, - OPTION_SYNC_INIT_CLIPBOARD, - ]; - // DEFAULT_LOCAL_SETTINGS, OVERWRITE_LOCAL_SETTINGS - pub const KEYS_LOCAL_SETTINGS: &[&str] = &[ - OPTION_THEME, - OPTION_LANGUAGE, - OPTION_ENABLE_CONFIRM_CLOSING_TABS, - OPTION_ENABLE_OPEN_NEW_CONNECTIONS_IN_TABS, - OPTION_TEXTURE_RENDER, - OPTION_SYNC_AB_WITH_RECENT_SESSIONS, - OPTION_SYNC_AB_TAGS, - OPTION_FILTER_AB_BY_INTERSECTION, - OPTION_REMOTE_MENUBAR_DRAG_LEFT, - OPTION_REMOTE_MENUBAR_DRAG_RIGHT, - OPTION_HIDE_AB_TAGS_PANEL, - OPTION_FLUTTER_REMOTE_MENUBAR_STATE, - OPTION_FLUTTER_PEER_SORTING, - OPTION_FLUTTER_PEER_TAB_INDEX, - OPTION_FLUTTER_PEER_TAB_ORDER, - OPTION_FLUTTER_PEER_TAB_VISIBLE, - OPTION_FLUTTER_PEER_CARD_UI_TYLE, - OPTION_FLUTTER_CURRENT_AB_NAME, - OPTION_DISABLE_FLOATING_WINDOW, - OPTION_FLOATING_WINDOW_SIZE, - OPTION_FLOATING_WINDOW_UNTOUCHABLE, - OPTION_FLOATING_WINDOW_TRANSPARENCY, - OPTION_FLOATING_WINDOW_SVG, - OPTION_KEEP_SCREEN_ON, - OPTION_DISABLE_GROUP_PANEL, - OPTION_PRE_ELEVATE_SERVICE, - OPTION_ALLOW_REMOTE_CM_MODIFICATION, - OPTION_ALLOW_AUTO_RECORD_OUTGOING, - OPTION_VIDEO_SAVE_DIRECTORY, - ]; - // DEFAULT_SETTINGS, OVERWRITE_SETTINGS - pub const KEYS_SETTINGS: &[&str] = &[ - OPTION_ACCESS_MODE, - OPTION_ENABLE_KEYBOARD, - OPTION_ENABLE_CLIPBOARD, - OPTION_ENABLE_FILE_TRANSFER, - OPTION_ENABLE_AUDIO, - OPTION_ENABLE_TUNNEL, - OPTION_ENABLE_REMOTE_RESTART, - OPTION_ENABLE_RECORD_SESSION, - OPTION_ENABLE_BLOCK_INPUT, - OPTION_ALLOW_REMOTE_CONFIG_MODIFICATION, - OPTION_ENABLE_LAN_DISCOVERY, - OPTION_DIRECT_SERVER, - OPTION_DIRECT_ACCESS_PORT, - OPTION_WHITELIST, - OPTION_ALLOW_AUTO_DISCONNECT, - OPTION_AUTO_DISCONNECT_TIMEOUT, - OPTION_ALLOW_ONLY_CONN_WINDOW_OPEN, - OPTION_ALLOW_AUTO_RECORD_INCOMING, - OPTION_ENABLE_ABR, - OPTION_ALLOW_REMOVE_WALLPAPER, - OPTION_ALLOW_ALWAYS_SOFTWARE_RENDER, - OPTION_ALLOW_LINUX_HEADLESS, - OPTION_ENABLE_HWCODEC, - OPTION_APPROVE_MODE, - OPTION_VERIFICATION_METHOD, - OPTION_PROXY_URL, - OPTION_PROXY_USERNAME, - OPTION_PROXY_PASSWORD, - OPTION_CUSTOM_RENDEZVOUS_SERVER, - OPTION_API_SERVER, - OPTION_KEY, - OPTION_PRESET_ADDRESS_BOOK_NAME, - OPTION_PRESET_ADDRESS_BOOK_TAG, - OPTION_ENABLE_DIRECTX_CAPTURE, - OPTION_ENABLE_ANDROID_SOFTWARE_ENCODING_HALF_SCALE, - OPTION_ENABLE_TRUSTED_DEVICES, - ]; - - // BUILDIN_SETTINGS - pub const KEYS_BUILDIN_SETTINGS: &[&str] = &[ - OPTION_DISPLAY_NAME, - OPTION_DISABLE_UDP, - OPTION_PRESET_USERNAME, - OPTION_PRESET_STRATEGY_NAME, - OPTION_REMOVE_PRESET_PASSWORD_WARNING, - OPTION_HIDE_SECURITY_SETTINGS, - OPTION_HIDE_NETWORK_SETTINGS, - OPTION_HIDE_SERVER_SETTINGS, - OPTION_HIDE_PROXY_SETTINGS, - OPTION_HIDE_USERNAME_ON_CARD, - OPTION_HIDE_HELP_CARDS, - OPTION_DEFAULT_CONNECT_PASSWORD, - OPTION_HIDE_TRAY, - OPTION_ONE_WAY_CLIPBOARD_REDIRECTION, - OPTION_ALLOW_LOGON_SCREEN_PASSWORD, - OPTION_ONE_WAY_FILE_TRANSFER, - ]; -} - -pub fn common_load< - T: serde::Serialize + serde::de::DeserializeOwned + Default + std::fmt::Debug, ->( - suffix: &str, -) -> T { - Config::load_::(suffix) -} - -pub fn common_store(config: &T, suffix: &str) { - Config::store_(config, suffix); -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_serialize() { - let cfg: Config = Default::default(); - let res = toml::to_string_pretty(&cfg); - assert!(res.is_ok()); - let cfg: PeerConfig = Default::default(); - let res = toml::to_string_pretty(&cfg); - assert!(res.is_ok()); - } - - #[test] - fn test_overwrite_settings() { - DEFAULT_SETTINGS - .write() - .unwrap() - .insert("b".to_string(), "a".to_string()); - DEFAULT_SETTINGS - .write() - .unwrap() - .insert("c".to_string(), "a".to_string()); - CONFIG2 - .write() - .unwrap() - .options - .insert("a".to_string(), "b".to_string()); - CONFIG2 - .write() - .unwrap() - .options - .insert("b".to_string(), "b".to_string()); - OVERWRITE_SETTINGS - .write() - .unwrap() - .insert("b".to_string(), "c".to_string()); - OVERWRITE_SETTINGS - .write() - .unwrap() - .insert("c".to_string(), "f".to_string()); - OVERWRITE_SETTINGS - .write() - .unwrap() - .insert("d".to_string(), "c".to_string()); - let mut res: HashMap = Default::default(); - res.insert("b".to_owned(), "c".to_string()); - res.insert("d".to_owned(), "c".to_string()); - res.insert("c".to_owned(), "a".to_string()); - Config::purify_options(&mut res); - assert!(res.len() == 0); - res.insert("b".to_owned(), "c".to_string()); - res.insert("d".to_owned(), "c".to_string()); - res.insert("c".to_owned(), "a".to_string()); - res.insert("f".to_owned(), "a".to_string()); - Config::purify_options(&mut res); - assert!(res.len() == 1); - res.insert("b".to_owned(), "c".to_string()); - res.insert("d".to_owned(), "c".to_string()); - res.insert("c".to_owned(), "a".to_string()); - res.insert("f".to_owned(), "a".to_string()); - res.insert("e".to_owned(), "d".to_string()); - Config::purify_options(&mut res); - assert!(res.len() == 2); - res.insert("b".to_owned(), "c".to_string()); - res.insert("d".to_owned(), "c".to_string()); - res.insert("c".to_owned(), "a".to_string()); - res.insert("f".to_owned(), "a".to_string()); - res.insert("c".to_owned(), "d".to_string()); - res.insert("d".to_owned(), "cc".to_string()); - Config::purify_options(&mut res); - DEFAULT_SETTINGS - .write() - .unwrap() - .insert("f".to_string(), "c".to_string()); - Config::purify_options(&mut res); - assert!(res.len() == 2); - DEFAULT_SETTINGS - .write() - .unwrap() - .insert("f".to_string(), "a".to_string()); - Config::purify_options(&mut res); - assert!(res.len() == 1); - let res = Config::get_options(); - assert!(res["a"] == "b"); - assert!(res["c"] == "f"); - assert!(res["b"] == "c"); - assert!(res["d"] == "c"); - assert!(Config::get_option("a") == "b"); - assert!(Config::get_option("c") == "f"); - assert!(Config::get_option("b") == "c"); - assert!(Config::get_option("d") == "c"); - DEFAULT_SETTINGS.write().unwrap().clear(); - OVERWRITE_SETTINGS.write().unwrap().clear(); - CONFIG2.write().unwrap().options.clear(); - - DEFAULT_LOCAL_SETTINGS - .write() - .unwrap() - .insert("b".to_string(), "a".to_string()); - DEFAULT_LOCAL_SETTINGS - .write() - .unwrap() - .insert("c".to_string(), "a".to_string()); - LOCAL_CONFIG - .write() - .unwrap() - .options - .insert("a".to_string(), "b".to_string()); - LOCAL_CONFIG - .write() - .unwrap() - .options - .insert("b".to_string(), "b".to_string()); - OVERWRITE_LOCAL_SETTINGS - .write() - .unwrap() - .insert("b".to_string(), "c".to_string()); - OVERWRITE_LOCAL_SETTINGS - .write() - .unwrap() - .insert("d".to_string(), "c".to_string()); - assert!(LocalConfig::get_option("a") == "b"); - assert!(LocalConfig::get_option("c") == "a"); - assert!(LocalConfig::get_option("b") == "c"); - assert!(LocalConfig::get_option("d") == "c"); - DEFAULT_LOCAL_SETTINGS.write().unwrap().clear(); - OVERWRITE_LOCAL_SETTINGS.write().unwrap().clear(); - LOCAL_CONFIG.write().unwrap().options.clear(); - - DEFAULT_DISPLAY_SETTINGS - .write() - .unwrap() - .insert("b".to_string(), "a".to_string()); - DEFAULT_DISPLAY_SETTINGS - .write() - .unwrap() - .insert("c".to_string(), "a".to_string()); - USER_DEFAULT_CONFIG - .write() - .unwrap() - .0 - .options - .insert("a".to_string(), "b".to_string()); - USER_DEFAULT_CONFIG - .write() - .unwrap() - .0 - .options - .insert("b".to_string(), "b".to_string()); - OVERWRITE_DISPLAY_SETTINGS - .write() - .unwrap() - .insert("b".to_string(), "c".to_string()); - OVERWRITE_DISPLAY_SETTINGS - .write() - .unwrap() - .insert("d".to_string(), "c".to_string()); - assert!(UserDefaultConfig::read("a") == "b"); - assert!(UserDefaultConfig::read("c") == "a"); - assert!(UserDefaultConfig::read("b") == "c"); - assert!(UserDefaultConfig::read("d") == "c"); - DEFAULT_DISPLAY_SETTINGS.write().unwrap().clear(); - OVERWRITE_DISPLAY_SETTINGS.write().unwrap().clear(); - LOCAL_CONFIG.write().unwrap().options.clear(); - } - - #[test] - fn test_config_deserialize() { - let wrong_type_str = r#" - id = true - enc_id = [] - password = 1 - salt = "123456" - key_pair = {} - key_confirmed = "1" - keys_confirmed = 1 - "#; - let cfg = toml::from_str::(wrong_type_str); - assert_eq!( - cfg, - Ok(Config { - salt: "123456".to_string(), - ..Default::default() - }) - ); - - let wrong_field_str = r#" - hello = "world" - key_confirmed = true - "#; - let cfg = toml::from_str::(wrong_field_str); - assert_eq!( - cfg, - Ok(Config { - key_confirmed: true, - ..Default::default() - }) - ); - } - - #[test] - fn test_peer_config_deserialize() { - let default_peer_config = toml::from_str::("").unwrap(); - // test custom_resolution - { - let wrong_type_str = r#" - view_style = "adaptive" - scroll_style = "scrollbar" - custom_resolutions = true - "#; - let mut cfg_to_compare = default_peer_config.clone(); - cfg_to_compare.view_style = "adaptive".to_string(); - cfg_to_compare.scroll_style = "scrollbar".to_string(); - let cfg = toml::from_str::(wrong_type_str); - assert_eq!(cfg, Ok(cfg_to_compare), "Failed to test wrong_type_str"); - - let wrong_type_str = r#" - view_style = "adaptive" - scroll_style = "scrollbar" - [custom_resolutions.0] - w = "1920" - h = 1080 - "#; - let mut cfg_to_compare = default_peer_config.clone(); - cfg_to_compare.view_style = "adaptive".to_string(); - cfg_to_compare.scroll_style = "scrollbar".to_string(); - let cfg = toml::from_str::(wrong_type_str); - assert_eq!(cfg, Ok(cfg_to_compare), "Failed to test wrong_type_str"); - - let wrong_field_str = r#" - [custom_resolutions.0] - w = 1920 - h = 1080 - hello = "world" - [ui_flutter] - "#; - let mut cfg_to_compare = default_peer_config.clone(); - cfg_to_compare.custom_resolutions = - HashMap::from([("0".to_string(), Resolution { w: 1920, h: 1080 })]); - let cfg = toml::from_str::(wrong_field_str); - assert_eq!(cfg, Ok(cfg_to_compare), "Failed to test wrong_field_str"); - } - } - - #[test] - fn test_store_load() { - let peerconfig_id = "123456789"; - let cfg: PeerConfig = Default::default(); - cfg.store(&peerconfig_id); - assert_eq!(PeerConfig::load(&peerconfig_id), cfg); - - #[cfg(not(windows))] - { - use std::os::unix::fs::PermissionsExt; - assert_eq!( - // ignore file type information by masking with 0o777 (see https://stackoverflow.com/a/50045872) - fs::metadata(PeerConfig::path(&peerconfig_id)) - .expect("reading metadata failed") - .permissions() - .mode() - & 0o777, - 0o600 - ); - } - } -} diff --git a/libs/hbb_common/src/fs.rs b/libs/hbb_common/src/fs.rs deleted file mode 100644 index 1488ffd93cf7..000000000000 --- a/libs/hbb_common/src/fs.rs +++ /dev/null @@ -1,953 +0,0 @@ -#[cfg(windows)] -use std::os::windows::prelude::*; -use std::path::{Path, PathBuf}; -use std::time::{Duration, SystemTime, UNIX_EPOCH}; - -use serde_derive::{Deserialize, Serialize}; -use serde_json::json; -use tokio::{fs::File, io::*}; - -use crate::{anyhow::anyhow, bail, get_version_number, message_proto::*, ResultType, Stream}; -// https://doc.rust-lang.org/std/os/windows/fs/trait.MetadataExt.html -use crate::{ - compress::{compress, decompress}, - config::Config, -}; - -pub fn read_dir(path: &Path, include_hidden: bool) -> ResultType { - let mut dir = FileDirectory { - path: get_string(path), - ..Default::default() - }; - #[cfg(windows)] - if "/" == &get_string(path) { - let drives = unsafe { winapi::um::fileapi::GetLogicalDrives() }; - for i in 0..32 { - if drives & (1 << i) != 0 { - let name = format!( - "{}:", - std::char::from_u32('A' as u32 + i as u32).unwrap_or('A') - ); - dir.entries.push(FileEntry { - name, - entry_type: FileType::DirDrive.into(), - ..Default::default() - }); - } - } - return Ok(dir); - } - for entry in path.read_dir()?.flatten() { - let p = entry.path(); - let name = p - .file_name() - .map(|p| p.to_str().unwrap_or("")) - .unwrap_or("") - .to_owned(); - if name.is_empty() { - continue; - } - let mut is_hidden = false; - let meta; - if let Ok(tmp) = std::fs::symlink_metadata(&p) { - meta = tmp; - } else { - continue; - } - // docs.microsoft.com/en-us/windows/win32/fileio/file-attribute-constants - #[cfg(windows)] - if meta.file_attributes() & 0x2 != 0 { - is_hidden = true; - } - #[cfg(not(windows))] - if name.find('.').unwrap_or(usize::MAX) == 0 { - is_hidden = true; - } - if is_hidden && !include_hidden { - continue; - } - let (entry_type, size) = { - if p.is_dir() { - if meta.file_type().is_symlink() { - (FileType::DirLink.into(), 0) - } else { - (FileType::Dir.into(), 0) - } - } else if meta.file_type().is_symlink() { - (FileType::FileLink.into(), 0) - } else { - (FileType::File.into(), meta.len()) - } - }; - let modified_time = meta - .modified() - .map(|x| { - x.duration_since(std::time::SystemTime::UNIX_EPOCH) - .map(|x| x.as_secs()) - .unwrap_or(0) - }) - .unwrap_or(0); - dir.entries.push(FileEntry { - name: get_file_name(&p), - entry_type, - is_hidden, - size, - modified_time, - ..Default::default() - }); - } - Ok(dir) -} - -#[inline] -pub fn get_file_name(p: &Path) -> String { - p.file_name() - .map(|p| p.to_str().unwrap_or("")) - .unwrap_or("") - .to_owned() -} - -#[inline] -pub fn get_string(path: &Path) -> String { - path.to_str().unwrap_or("").to_owned() -} - -#[inline] -pub fn get_path(path: &str) -> PathBuf { - Path::new(path).to_path_buf() -} - -#[inline] -pub fn get_home_as_string() -> String { - get_string(&Config::get_home()) -} - -fn read_dir_recursive( - path: &Path, - prefix: &Path, - include_hidden: bool, -) -> ResultType> { - let mut files = Vec::new(); - if path.is_dir() { - // to-do: symbol link handling, cp the link rather than the content - // to-do: file mode, for unix - let fd = read_dir(path, include_hidden)?; - for entry in fd.entries.iter() { - match entry.entry_type.enum_value() { - Ok(FileType::File) => { - let mut entry = entry.clone(); - entry.name = get_string(&prefix.join(entry.name)); - files.push(entry); - } - Ok(FileType::Dir) => { - if let Ok(mut tmp) = read_dir_recursive( - &path.join(&entry.name), - &prefix.join(&entry.name), - include_hidden, - ) { - for entry in tmp.drain(0..) { - files.push(entry); - } - } - } - _ => {} - } - } - Ok(files) - } else if path.is_file() { - let (size, modified_time) = if let Ok(meta) = std::fs::metadata(path) { - ( - meta.len(), - meta.modified() - .map(|x| { - x.duration_since(std::time::SystemTime::UNIX_EPOCH) - .map(|x| x.as_secs()) - .unwrap_or(0) - }) - .unwrap_or(0), - ) - } else { - (0, 0) - }; - files.push(FileEntry { - entry_type: FileType::File.into(), - size, - modified_time, - ..Default::default() - }); - Ok(files) - } else { - bail!("Not exists"); - } -} - -pub fn get_recursive_files(path: &str, include_hidden: bool) -> ResultType> { - read_dir_recursive(&get_path(path), &get_path(""), include_hidden) -} - -fn read_empty_dirs_recursive( - path: &Path, - prefix: &Path, - include_hidden: bool, -) -> ResultType> { - let mut dirs = Vec::new(); - if path.is_dir() { - // to-do: symbol link handling, cp the link rather than the content - // to-do: file mode, for unix - let fd = read_dir(path, include_hidden)?; - if fd.entries.is_empty() { - dirs.push(fd); - } else { - for entry in fd.entries.iter() { - match entry.entry_type.enum_value() { - Ok(FileType::Dir) => { - if let Ok(mut tmp) = read_empty_dirs_recursive( - &path.join(&entry.name), - &prefix.join(&entry.name), - include_hidden, - ) { - for entry in tmp.drain(0..) { - dirs.push(entry); - } - } - } - _ => {} - } - } - } - Ok(dirs) - } else if path.is_file() { - Ok(dirs) - } else { - bail!("Not exists"); - } -} - -pub fn get_empty_dirs_recursive( - path: &str, - include_hidden: bool, -) -> ResultType> { - read_empty_dirs_recursive(&get_path(path), &get_path(""), include_hidden) -} - -#[inline] -pub fn is_file_exists(file_path: &str) -> bool { - return Path::new(file_path).exists(); -} - -#[inline] -pub fn can_enable_overwrite_detection(version: i64) -> bool { - version >= get_version_number("1.1.10") -} - -#[derive(Default, Serialize, Debug)] -#[serde(rename_all = "camelCase")] -pub struct TransferJob { - pub id: i32, - pub remote: String, - pub path: PathBuf, - pub show_hidden: bool, - pub is_remote: bool, - pub is_last_job: bool, - pub file_num: i32, - #[serde(skip_serializing)] - pub files: Vec, - pub conn_id: i32, // server only - - #[serde(skip_serializing)] - file: Option, - pub total_size: u64, - finished_size: u64, - transferred: u64, - enable_overwrite_detection: bool, - file_confirmed: bool, - // indicating the last file is skipped - file_skipped: bool, - file_is_waiting: bool, - default_overwrite_strategy: Option, -} - -#[derive(Debug, Default, Serialize, Deserialize, Clone)] -pub struct TransferJobMeta { - #[serde(default)] - pub id: i32, - #[serde(default)] - pub remote: String, - #[serde(default)] - pub to: String, - #[serde(default)] - pub show_hidden: bool, - #[serde(default)] - pub file_num: i32, - #[serde(default)] - pub is_remote: bool, -} - -#[derive(Debug, Default, Serialize, Deserialize, Clone)] -pub struct RemoveJobMeta { - #[serde(default)] - pub path: String, - #[serde(default)] - pub is_remote: bool, - #[serde(default)] - pub no_confirm: bool, -} - -#[inline] -fn get_ext(name: &str) -> &str { - if let Some(i) = name.rfind('.') { - return &name[i + 1..]; - } - "" -} - -#[inline] -fn is_compressed_file(name: &str) -> bool { - let compressed_exts = ["xz", "gz", "zip", "7z", "rar", "bz2", "tgz", "png", "jpg"]; - let ext = get_ext(name); - compressed_exts.contains(&ext) -} - -impl TransferJob { - #[allow(clippy::too_many_arguments)] - pub fn new_write( - id: i32, - remote: String, - path: String, - file_num: i32, - show_hidden: bool, - is_remote: bool, - files: Vec, - enable_overwrite_detection: bool, - ) -> Self { - log::info!("new write {}", path); - let total_size = files.iter().map(|x| x.size).sum(); - Self { - id, - remote, - path: get_path(&path), - file_num, - show_hidden, - is_remote, - files, - total_size, - enable_overwrite_detection, - ..Default::default() - } - } - - pub fn new_read( - id: i32, - remote: String, - path: String, - file_num: i32, - show_hidden: bool, - is_remote: bool, - enable_overwrite_detection: bool, - ) -> ResultType { - log::info!("new read {}", path); - let files = get_recursive_files(&path, show_hidden)?; - let total_size = files.iter().map(|x| x.size).sum(); - Ok(Self { - id, - remote, - path: get_path(&path), - file_num, - show_hidden, - is_remote, - files, - total_size, - enable_overwrite_detection, - ..Default::default() - }) - } - - #[inline] - pub fn files(&self) -> &Vec { - &self.files - } - - #[inline] - pub fn set_files(&mut self, files: Vec) { - self.files = files; - } - - #[inline] - pub fn id(&self) -> i32 { - self.id - } - - #[inline] - pub fn total_size(&self) -> u64 { - self.total_size - } - - #[inline] - pub fn finished_size(&self) -> u64 { - self.finished_size - } - - #[inline] - pub fn transferred(&self) -> u64 { - self.transferred - } - - #[inline] - pub fn file_num(&self) -> i32 { - self.file_num - } - - pub fn modify_time(&self) { - let file_num = self.file_num as usize; - if file_num < self.files.len() { - let entry = &self.files[file_num]; - let path = self.join(&entry.name); - let download_path = format!("{}.download", get_string(&path)); - std::fs::rename(download_path, &path).ok(); - filetime::set_file_mtime( - &path, - filetime::FileTime::from_unix_time(entry.modified_time as _, 0), - ) - .ok(); - } - } - - pub fn remove_download_file(&self) { - let file_num = self.file_num as usize; - if file_num < self.files.len() { - let entry = &self.files[file_num]; - let path = self.join(&entry.name); - let download_path = format!("{}.download", get_string(&path)); - std::fs::remove_file(download_path).ok(); - } - } - - pub async fn write(&mut self, block: FileTransferBlock) -> ResultType<()> { - if block.id != self.id { - bail!("Wrong id"); - } - let file_num = block.file_num as usize; - if file_num >= self.files.len() { - bail!("Wrong file number"); - } - if file_num != self.file_num as usize || self.file.is_none() { - self.modify_time(); - if let Some(file) = self.file.as_mut() { - file.sync_all().await?; - } - self.file_num = block.file_num; - let entry = &self.files[file_num]; - let path = self.join(&entry.name); - if let Some(p) = path.parent() { - std::fs::create_dir_all(p).ok(); - } - let path = format!("{}.download", get_string(&path)); - self.file = Some(File::create(&path).await?); - } - if block.compressed { - let tmp = decompress(&block.data); - self.file - .as_mut() - .ok_or(anyhow!("file is None"))? - .write_all(&tmp) - .await?; - self.finished_size += tmp.len() as u64; - } else { - self.file - .as_mut() - .ok_or(anyhow!("file is None"))? - .write_all(&block.data) - .await?; - self.finished_size += block.data.len() as u64; - } - self.transferred += block.data.len() as u64; - Ok(()) - } - - #[inline] - pub fn join(&self, name: &str) -> PathBuf { - if name.is_empty() { - self.path.clone() - } else { - self.path.join(name) - } - } - - pub async fn read(&mut self, stream: &mut Stream) -> ResultType> { - let file_num = self.file_num as usize; - if file_num >= self.files.len() { - self.file.take(); - return Ok(None); - } - let name = &self.files[file_num].name; - if self.file.is_none() { - match File::open(self.join(name)).await { - Ok(file) => { - self.file = Some(file); - self.file_confirmed = false; - self.file_is_waiting = false; - } - Err(err) => { - self.file_num += 1; - self.file_confirmed = false; - self.file_is_waiting = false; - return Err(err.into()); - } - } - } - if self.enable_overwrite_detection && !self.file_confirmed() { - if !self.file_is_waiting() { - self.send_current_digest(stream).await?; - self.set_file_is_waiting(true); - } - return Ok(None); - } - const BUF_SIZE: usize = 128 * 1024; - let mut buf: Vec = vec![0; BUF_SIZE]; - let mut compressed = false; - let mut offset: usize = 0; - loop { - match self - .file - .as_mut() - .ok_or(anyhow!("file is None"))? - .read(&mut buf[offset..]) - .await - { - Err(err) => { - self.file_num += 1; - self.file = None; - self.file_confirmed = false; - self.file_is_waiting = false; - return Err(err.into()); - } - Ok(n) => { - offset += n; - if n == 0 || offset == BUF_SIZE { - break; - } - } - } - } - unsafe { buf.set_len(offset) }; - if offset == 0 { - self.file_num += 1; - self.file = None; - self.file_confirmed = false; - self.file_is_waiting = false; - } else { - self.finished_size += offset as u64; - if !is_compressed_file(name) { - let tmp = compress(&buf); - if tmp.len() < buf.len() { - buf = tmp; - compressed = true; - } - } - self.transferred += buf.len() as u64; - } - Ok(Some(FileTransferBlock { - id: self.id, - file_num: file_num as _, - data: buf.into(), - compressed, - ..Default::default() - })) - } - - async fn send_current_digest(&mut self, stream: &mut Stream) -> ResultType<()> { - let mut msg = Message::new(); - let mut resp = FileResponse::new(); - let meta = self - .file - .as_ref() - .ok_or(anyhow!("file is None"))? - .metadata() - .await?; - let last_modified = meta - .modified()? - .duration_since(SystemTime::UNIX_EPOCH)? - .as_secs(); - resp.set_digest(FileTransferDigest { - id: self.id, - file_num: self.file_num, - last_modified, - file_size: meta.len(), - ..Default::default() - }); - msg.set_file_response(resp); - stream.send(&msg).await?; - log::info!( - "id: {}, file_num: {}, digest message is sent. waiting for confirm. msg: {:?}", - self.id, - self.file_num, - msg - ); - Ok(()) - } - - pub fn set_overwrite_strategy(&mut self, overwrite_strategy: Option) { - self.default_overwrite_strategy = overwrite_strategy; - } - - pub fn default_overwrite_strategy(&self) -> Option { - self.default_overwrite_strategy - } - - pub fn set_file_confirmed(&mut self, file_confirmed: bool) { - log::info!("id: {}, file_confirmed: {}", self.id, file_confirmed); - self.file_confirmed = file_confirmed; - self.file_skipped = false; - } - - pub fn set_file_is_waiting(&mut self, file_is_waiting: bool) { - self.file_is_waiting = file_is_waiting; - } - - #[inline] - pub fn file_is_waiting(&self) -> bool { - self.file_is_waiting - } - - #[inline] - pub fn file_confirmed(&self) -> bool { - self.file_confirmed - } - - /// Indicating whether the last file is skipped - #[inline] - pub fn file_skipped(&self) -> bool { - self.file_skipped - } - - /// Indicating whether the whole task is skipped - #[inline] - pub fn job_skipped(&self) -> bool { - self.file_skipped() && self.files.len() == 1 - } - - /// Check whether the job is completed after `read` returns `None` - /// This is a helper function which gives additional lifecycle when the job reads `None`. - /// If returns `true`, it means we can delete the job automatically. `False` otherwise. - /// - /// [`Note`] - /// Conditions: - /// 1. Files are not waiting for confirmation by peers. - #[inline] - pub fn job_completed(&self) -> bool { - // has no error, Condition 2 - !self.enable_overwrite_detection || (!self.file_confirmed && !self.file_is_waiting) - } - - /// Get job error message, useful for getting status when job had finished - pub fn job_error(&self) -> Option { - if self.job_skipped() { - return Some("skipped".to_string()); - } - None - } - - pub fn set_file_skipped(&mut self) -> bool { - log::debug!("skip file {} in job {}", self.file_num, self.id); - self.file.take(); - self.set_file_confirmed(false); - self.set_file_is_waiting(false); - self.file_num += 1; - self.file_skipped = true; - true - } - - pub fn confirm(&mut self, r: &FileTransferSendConfirmRequest) -> bool { - if self.file_num() != r.file_num { - log::info!("file num truncated, ignoring"); - } else { - match r.union { - Some(file_transfer_send_confirm_request::Union::Skip(s)) => { - if s { - self.set_file_skipped(); - } else { - self.set_file_confirmed(true); - } - } - Some(file_transfer_send_confirm_request::Union::OffsetBlk(_offset)) => { - self.set_file_confirmed(true); - } - _ => {} - } - } - true - } - - #[inline] - pub fn gen_meta(&self) -> TransferJobMeta { - TransferJobMeta { - id: self.id, - remote: self.remote.to_string(), - to: self.path.to_string_lossy().to_string(), - file_num: self.file_num, - show_hidden: self.show_hidden, - is_remote: self.is_remote, - } - } -} - -#[inline] -pub fn new_error(id: i32, err: T, file_num: i32) -> Message { - let mut resp = FileResponse::new(); - resp.set_error(FileTransferError { - id, - error: err.to_string(), - file_num, - ..Default::default() - }); - let mut msg_out = Message::new(); - msg_out.set_file_response(resp); - msg_out -} - -#[inline] -pub fn new_dir(id: i32, path: String, files: Vec) -> Message { - let mut resp = FileResponse::new(); - resp.set_dir(FileDirectory { - id, - path, - entries: files, - ..Default::default() - }); - let mut msg_out = Message::new(); - msg_out.set_file_response(resp); - msg_out -} - -#[inline] -pub fn new_block(block: FileTransferBlock) -> Message { - let mut resp = FileResponse::new(); - resp.set_block(block); - let mut msg_out = Message::new(); - msg_out.set_file_response(resp); - msg_out -} - -#[inline] -pub fn new_send_confirm(r: FileTransferSendConfirmRequest) -> Message { - let mut msg_out = Message::new(); - let mut action = FileAction::new(); - action.set_send_confirm(r); - msg_out.set_file_action(action); - msg_out -} - -#[inline] -pub fn new_receive( - id: i32, - path: String, - file_num: i32, - files: Vec, - total_size: u64, -) -> Message { - let mut action = FileAction::new(); - action.set_receive(FileTransferReceiveRequest { - id, - path, - files, - file_num, - total_size, - ..Default::default() - }); - let mut msg_out = Message::new(); - msg_out.set_file_action(action); - msg_out -} - -#[inline] -pub fn new_send(id: i32, path: String, file_num: i32, include_hidden: bool) -> Message { - log::info!("new send: {}, id: {}", path, id); - let mut action = FileAction::new(); - action.set_send(FileTransferSendRequest { - id, - path, - include_hidden, - file_num, - ..Default::default() - }); - let mut msg_out = Message::new(); - msg_out.set_file_action(action); - msg_out -} - -#[inline] -pub fn new_done(id: i32, file_num: i32) -> Message { - let mut resp = FileResponse::new(); - resp.set_done(FileTransferDone { - id, - file_num, - ..Default::default() - }); - let mut msg_out = Message::new(); - msg_out.set_file_response(resp); - msg_out -} - -#[inline] -pub fn remove_job(id: i32, jobs: &mut Vec) { - *jobs = jobs.drain(0..).filter(|x| x.id() != id).collect(); -} - -#[inline] -pub fn get_job(id: i32, jobs: &mut [TransferJob]) -> Option<&mut TransferJob> { - jobs.iter_mut().find(|x| x.id() == id) -} - -#[inline] -pub fn get_job_immutable(id: i32, jobs: &[TransferJob]) -> Option<&TransferJob> { - jobs.iter().find(|x| x.id() == id) -} - -pub async fn handle_read_jobs( - jobs: &mut Vec, - stream: &mut crate::Stream, -) -> ResultType { - let mut job_log = Default::default(); - let mut finished = Vec::new(); - for job in jobs.iter_mut() { - if job.is_last_job { - continue; - } - match job.read(stream).await { - Err(err) => { - stream - .send(&new_error(job.id(), err, job.file_num())) - .await?; - } - Ok(Some(block)) => { - stream.send(&new_block(block)).await?; - } - Ok(None) => { - if job.job_completed() { - job_log = serialize_transfer_job(job, true, false, ""); - finished.push(job.id()); - match job.job_error() { - Some(err) => { - job_log = serialize_transfer_job(job, false, false, &err); - stream - .send(&new_error(job.id(), err, job.file_num())) - .await? - } - None => stream.send(&new_done(job.id(), job.file_num())).await?, - } - } else { - // waiting confirmation. - } - } - } - } - for id in finished { - remove_job(id, jobs); - } - Ok(job_log) -} - -pub fn remove_all_empty_dir(path: &Path) -> ResultType<()> { - let fd = read_dir(path, true)?; - for entry in fd.entries.iter() { - match entry.entry_type.enum_value() { - Ok(FileType::Dir) => { - remove_all_empty_dir(&path.join(&entry.name)).ok(); - } - Ok(FileType::DirLink) | Ok(FileType::FileLink) => { - std::fs::remove_file(path.join(&entry.name)).ok(); - } - _ => {} - } - } - std::fs::remove_dir(path).ok(); - Ok(()) -} - -#[inline] -pub fn remove_file(file: &str) -> ResultType<()> { - std::fs::remove_file(get_path(file))?; - Ok(()) -} - -#[inline] -pub fn create_dir(dir: &str) -> ResultType<()> { - std::fs::create_dir_all(get_path(dir))?; - Ok(()) -} - -#[inline] -pub fn rename_file(path: &str, new_name: &str) -> ResultType<()> { - let path = std::path::Path::new(&path); - if path.exists() { - let dir = path - .parent() - .ok_or(anyhow!("Parent directoy of {path:?} not exists"))?; - let new_path = dir.join(&new_name); - std::fs::rename(&path, &new_path)?; - Ok(()) - } else { - bail!("{path:?} not exists"); - } -} - -#[inline] -pub fn transform_windows_path(entries: &mut Vec) { - for entry in entries { - entry.name = entry.name.replace('\\', "/"); - } -} - -pub enum DigestCheckResult { - IsSame, - NeedConfirm(FileTransferDigest), - NoSuchFile, -} - -#[inline] -pub fn is_write_need_confirmation( - file_path: &str, - digest: &FileTransferDigest, -) -> ResultType { - let path = Path::new(file_path); - if path.exists() && path.is_file() { - let metadata = std::fs::metadata(path)?; - let modified_time = metadata.modified()?; - let remote_mt = Duration::from_secs(digest.last_modified); - let local_mt = modified_time.duration_since(UNIX_EPOCH)?; - // [Note] - // We decide to give the decision whether to override the existing file to users, - // which obey the behavior of the file manager in our system. - let mut is_identical = false; - if remote_mt == local_mt && digest.file_size == metadata.len() { - is_identical = true; - } - Ok(DigestCheckResult::NeedConfirm(FileTransferDigest { - id: digest.id, - file_num: digest.file_num, - last_modified: local_mt.as_secs(), - file_size: metadata.len(), - is_identical, - ..Default::default() - })) - } else { - Ok(DigestCheckResult::NoSuchFile) - } -} - -pub fn serialize_transfer_jobs(jobs: &[TransferJob]) -> String { - let mut v = vec![]; - for job in jobs { - let value = serde_json::to_value(job).unwrap_or_default(); - v.push(value); - } - serde_json::to_string(&v).unwrap_or_default() -} - -pub fn serialize_transfer_job(job: &TransferJob, done: bool, cancel: bool, error: &str) -> String { - let mut value = serde_json::to_value(job).unwrap_or_default(); - value["done"] = json!(done); - value["cancel"] = json!(cancel); - value["error"] = json!(error); - serde_json::to_string(&value).unwrap_or_default() -} diff --git a/libs/hbb_common/src/keyboard.rs b/libs/hbb_common/src/keyboard.rs deleted file mode 100644 index 10979f520e94..000000000000 --- a/libs/hbb_common/src/keyboard.rs +++ /dev/null @@ -1,39 +0,0 @@ -use std::{fmt, slice::Iter, str::FromStr}; - -use crate::protos::message::KeyboardMode; - -impl fmt::Display for KeyboardMode { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - KeyboardMode::Legacy => write!(f, "legacy"), - KeyboardMode::Map => write!(f, "map"), - KeyboardMode::Translate => write!(f, "translate"), - KeyboardMode::Auto => write!(f, "auto"), - } - } -} - -impl FromStr for KeyboardMode { - type Err = (); - fn from_str(s: &str) -> Result { - match s { - "legacy" => Ok(KeyboardMode::Legacy), - "map" => Ok(KeyboardMode::Map), - "translate" => Ok(KeyboardMode::Translate), - "auto" => Ok(KeyboardMode::Auto), - _ => Err(()), - } - } -} - -impl KeyboardMode { - pub fn iter() -> Iter<'static, KeyboardMode> { - static KEYBOARD_MODES: [KeyboardMode; 4] = [ - KeyboardMode::Legacy, - KeyboardMode::Map, - KeyboardMode::Translate, - KeyboardMode::Auto, - ]; - KEYBOARD_MODES.iter() - } -} diff --git a/libs/hbb_common/src/lib.rs b/libs/hbb_common/src/lib.rs deleted file mode 100644 index 36a68550fa52..000000000000 --- a/libs/hbb_common/src/lib.rs +++ /dev/null @@ -1,500 +0,0 @@ -pub mod compress; -pub mod platform; -pub mod protos; -pub use bytes; -use config::Config; -pub use futures; -pub use protobuf; -pub use protos::message as message_proto; -pub use protos::rendezvous as rendezvous_proto; -use std::{ - fs::File, - io::{self, BufRead}, - net::{IpAddr, Ipv4Addr, SocketAddr, SocketAddrV4}, - path::Path, - time::{self, SystemTime, UNIX_EPOCH}, -}; -pub use tokio; -pub use tokio_util; -pub mod proxy; -pub mod socket_client; -pub mod tcp; -pub mod udp; -pub use env_logger; -pub use log; -pub mod bytes_codec; -pub use anyhow::{self, bail}; -pub use futures_util; -pub mod config; -pub mod fs; -pub mod mem; -pub use lazy_static; -#[cfg(not(any(target_os = "android", target_os = "ios")))] -pub use mac_address; -pub use rand; -pub use regex; -pub use sodiumoxide; -pub use tokio_socks; -pub use tokio_socks::IntoTargetAddr; -pub use tokio_socks::TargetAddr; -pub mod password_security; -pub use chrono; -pub use directories_next; -pub use libc; -pub mod keyboard; -pub use base64; -#[cfg(not(any(target_os = "android", target_os = "ios")))] -pub use dlopen; -#[cfg(not(any(target_os = "android", target_os = "ios")))] -pub use machine_uid; -pub use serde_derive; -pub use serde_json; -pub use sysinfo; -pub use thiserror; -pub use toml; -pub use uuid; - -pub type Stream = tcp::FramedStream; -pub type SessionID = uuid::Uuid; - -#[inline] -pub async fn sleep(sec: f32) { - tokio::time::sleep(time::Duration::from_secs_f32(sec)).await; -} - -#[macro_export] -macro_rules! allow_err { - ($e:expr) => { - if let Err(err) = $e { - log::debug!( - "{:?}, {}:{}:{}:{}", - err, - module_path!(), - file!(), - line!(), - column!() - ); - } else { - } - }; - - ($e:expr, $($arg:tt)*) => { - if let Err(err) = $e { - log::debug!( - "{:?}, {}, {}:{}:{}:{}", - err, - format_args!($($arg)*), - module_path!(), - file!(), - line!(), - column!() - ); - } else { - } - }; -} - -#[inline] -pub fn timeout(ms: u64, future: T) -> tokio::time::Timeout { - tokio::time::timeout(std::time::Duration::from_millis(ms), future) -} - -pub type ResultType = anyhow::Result; - -/// Certain router and firewalls scan the packet and if they -/// find an IP address belonging to their pool that they use to do the NAT mapping/translation, so here we mangle the ip address - -pub struct AddrMangle(); - -#[inline] -pub fn try_into_v4(addr: SocketAddr) -> SocketAddr { - match addr { - SocketAddr::V6(v6) if !addr.ip().is_loopback() => { - if let Some(v4) = v6.ip().to_ipv4() { - SocketAddr::new(IpAddr::V4(v4), addr.port()) - } else { - addr - } - } - _ => addr, - } -} - -impl AddrMangle { - pub fn encode(addr: SocketAddr) -> Vec { - // not work with [:1]: - let addr = try_into_v4(addr); - match addr { - SocketAddr::V4(addr_v4) => { - let tm = (SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap_or(std::time::Duration::ZERO) - .as_micros() as u32) as u128; - let ip = u32::from_le_bytes(addr_v4.ip().octets()) as u128; - let port = addr.port() as u128; - let v = ((ip + tm) << 49) | (tm << 17) | (port + (tm & 0xFFFF)); - let bytes = v.to_le_bytes(); - let mut n_padding = 0; - for i in bytes.iter().rev() { - if i == &0u8 { - n_padding += 1; - } else { - break; - } - } - bytes[..(16 - n_padding)].to_vec() - } - SocketAddr::V6(addr_v6) => { - let mut x = addr_v6.ip().octets().to_vec(); - let port: [u8; 2] = addr_v6.port().to_le_bytes(); - x.push(port[0]); - x.push(port[1]); - x - } - } - } - - pub fn decode(bytes: &[u8]) -> SocketAddr { - use std::convert::TryInto; - - if bytes.len() > 16 { - if bytes.len() != 18 { - return Config::get_any_listen_addr(false); - } - let tmp: [u8; 2] = bytes[16..].try_into().unwrap_or_default(); - let port = u16::from_le_bytes(tmp); - let tmp: [u8; 16] = bytes[..16].try_into().unwrap_or_default(); - let ip = std::net::Ipv6Addr::from(tmp); - return SocketAddr::new(IpAddr::V6(ip), port); - } - let mut padded = [0u8; 16]; - padded[..bytes.len()].copy_from_slice(bytes); - let number = u128::from_le_bytes(padded); - let tm = (number >> 17) & (u32::max_value() as u128); - let ip = (((number >> 49) - tm) as u32).to_le_bytes(); - let port = (number & 0xFFFFFF) - (tm & 0xFFFF); - SocketAddr::V4(SocketAddrV4::new( - Ipv4Addr::new(ip[0], ip[1], ip[2], ip[3]), - port as u16, - )) - } -} - -pub fn get_version_from_url(url: &str) -> String { - let n = url.chars().count(); - let a = url.chars().rev().position(|x| x == '-'); - if let Some(a) = a { - let b = url.chars().rev().position(|x| x == '.'); - if let Some(b) = b { - if a > b { - if url - .chars() - .skip(n - b) - .collect::() - .parse::() - .is_ok() - { - return url.chars().skip(n - a).collect(); - } else { - return url.chars().skip(n - a).take(a - b - 1).collect(); - } - } else { - return url.chars().skip(n - a).collect(); - } - } - } - "".to_owned() -} - -pub fn gen_version() { - println!("cargo:rerun-if-changed=Cargo.toml"); - use std::io::prelude::*; - let mut file = File::create("./src/version.rs").unwrap(); - for line in read_lines("Cargo.toml").unwrap().flatten() { - let ab: Vec<&str> = line.split('=').map(|x| x.trim()).collect(); - if ab.len() == 2 && ab[0] == "version" { - file.write_all(format!("pub const VERSION: &str = {};\n", ab[1]).as_bytes()) - .ok(); - break; - } - } - // generate build date - let build_date = format!("{}", chrono::Local::now().format("%Y-%m-%d %H:%M")); - file.write_all( - format!("#[allow(dead_code)]\npub const BUILD_DATE: &str = \"{build_date}\";\n").as_bytes(), - ) - .ok(); - file.sync_all().ok(); -} - -fn read_lines

    (filename: P) -> io::Result>> -where - P: AsRef, -{ - let file = File::open(filename)?; - Ok(io::BufReader::new(file).lines()) -} - -pub fn is_valid_custom_id(id: &str) -> bool { - regex::Regex::new(r"^[a-zA-Z]\w{5,15}$") - .unwrap() - .is_match(id) -} - -// Support 1.1.10-1, the number after - is a patch version. -pub fn get_version_number(v: &str) -> i64 { - let mut versions = v.split('-'); - - let mut n = 0; - - // The first part is the version number. - // 1.1.10 -> 1001100, 1.2.3 -> 1001030, multiple the last number by 10 - // to leave space for patch version. - if let Some(v) = versions.next() { - let mut last = 0; - for x in v.split('.') { - last = x.parse::().unwrap_or(0); - n = n * 1000 + last; - } - n -= last; - n += last * 10; - } - - if let Some(v) = versions.next() { - n += v.parse::().unwrap_or(0); - } - - // Ignore the rest - - n -} - -pub fn get_modified_time(path: &std::path::Path) -> SystemTime { - std::fs::metadata(path) - .map(|m| m.modified().unwrap_or(UNIX_EPOCH)) - .unwrap_or(UNIX_EPOCH) -} - -pub fn get_created_time(path: &std::path::Path) -> SystemTime { - std::fs::metadata(path) - .map(|m| m.created().unwrap_or(UNIX_EPOCH)) - .unwrap_or(UNIX_EPOCH) -} - -pub fn get_exe_time() -> SystemTime { - std::env::current_exe().map_or(UNIX_EPOCH, |path| { - let m = get_modified_time(&path); - let c = get_created_time(&path); - if m > c { - m - } else { - c - } - }) -} - -pub fn get_uuid() -> Vec { - #[cfg(not(any(target_os = "android", target_os = "ios")))] - if let Ok(id) = machine_uid::get() { - return id.into(); - } - Config::get_key_pair().1 -} - -#[inline] -pub fn get_time() -> i64 { - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .map(|d| d.as_millis()) - .unwrap_or(0) as _ -} - -#[inline] -pub fn is_ipv4_str(id: &str) -> bool { - if let Ok(reg) = regex::Regex::new( - r"^(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(:\d+)?$", - ) { - reg.is_match(id) - } else { - false - } -} - -#[inline] -pub fn is_ipv6_str(id: &str) -> bool { - if let Ok(reg) = regex::Regex::new( - r"^((([a-fA-F0-9]{1,4}:{1,2})+[a-fA-F0-9]{1,4})|(\[([a-fA-F0-9]{1,4}:{1,2})+[a-fA-F0-9]{1,4}\]:\d+))$", - ) { - reg.is_match(id) - } else { - false - } -} - -#[inline] -pub fn is_ip_str(id: &str) -> bool { - is_ipv4_str(id) || is_ipv6_str(id) -} - -#[inline] -pub fn is_domain_port_str(id: &str) -> bool { - // modified regex for RFC1123 hostname. check https://stackoverflow.com/a/106223 for original version for hostname. - // according to [TLD List](https://data.iana.org/TLD/tlds-alpha-by-domain.txt) version 2023011700, - // there is no digits in TLD, and length is 2~63. - if let Ok(reg) = regex::Regex::new( - r"(?i)^([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z][a-z-]{0,61}[a-z]:\d{1,5}$", - ) { - reg.is_match(id) - } else { - false - } -} - -pub fn init_log(_is_async: bool, _name: &str) -> Option { - static INIT: std::sync::Once = std::sync::Once::new(); - #[allow(unused_mut)] - let mut logger_holder: Option = None; - INIT.call_once(|| { - #[cfg(debug_assertions)] - { - use env_logger::*; - init_from_env(Env::default().filter_or(DEFAULT_FILTER_ENV, "info")); - } - #[cfg(not(debug_assertions))] - { - // https://docs.rs/flexi_logger/latest/flexi_logger/error_info/index.html#write - // though async logger more efficient, but it also causes more problems, disable it for now - let mut path = config::Config::log_path(); - #[cfg(target_os = "android")] - if !config::Config::get_home().exists() { - return; - } - if !_name.is_empty() { - path.push(_name); - } - use flexi_logger::*; - if let Ok(x) = Logger::try_with_env_or_str("debug") { - logger_holder = x - .log_to_file(FileSpec::default().directory(path)) - .write_mode(if _is_async { - WriteMode::Async - } else { - WriteMode::Direct - }) - .format(opt_format) - .rotate( - Criterion::Age(Age::Day), - Naming::Timestamps, - Cleanup::KeepLogFiles(31), - ) - .start() - .ok(); - } - } - }); - logger_holder -} - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn test_mangle() { - let addr = SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(192, 168, 16, 32), 21116)); - assert_eq!(addr, AddrMangle::decode(&AddrMangle::encode(addr))); - - let addr = "[2001:db8::1]:8080".parse::().unwrap(); - assert_eq!(addr, AddrMangle::decode(&AddrMangle::encode(addr))); - - let addr = "[2001:db8:ff::1111]:80".parse::().unwrap(); - assert_eq!(addr, AddrMangle::decode(&AddrMangle::encode(addr))); - } - - #[test] - fn test_allow_err() { - allow_err!(Err("test err") as Result<(), &str>); - allow_err!( - Err("test err with msg") as Result<(), &str>, - "prompt {}", - "failed" - ); - } - - #[test] - fn test_ipv6() { - assert!(is_ipv6_str("1:2:3")); - assert!(is_ipv6_str("[ab:2:3]:12")); - assert!(is_ipv6_str("[ABEF:2a:3]:12")); - assert!(!is_ipv6_str("[ABEG:2a:3]:12")); - assert!(!is_ipv6_str("1[ab:2:3]:12")); - assert!(!is_ipv6_str("1.1.1.1")); - assert!(is_ip_str("1.1.1.1")); - assert!(!is_ipv6_str("1:2:")); - assert!(is_ipv6_str("1:2::0")); - assert!(is_ipv6_str("[1:2::0]:1")); - assert!(!is_ipv6_str("[1:2::0]:")); - assert!(!is_ipv6_str("1:2::0]:1")); - } - - #[test] - fn test_ipv4() { - assert!(is_ipv4_str("1.2.3.4")); - assert!(is_ipv4_str("1.2.3.4:90")); - assert!(is_ipv4_str("192.168.0.1")); - assert!(is_ipv4_str("0.0.0.0")); - assert!(is_ipv4_str("255.255.255.255")); - assert!(!is_ipv4_str("256.0.0.0")); - assert!(!is_ipv4_str("256.256.256.256")); - assert!(!is_ipv4_str("1:2:")); - assert!(!is_ipv4_str("192.168.0.256")); - assert!(!is_ipv4_str("192.168.0.1/24")); - assert!(!is_ipv4_str("192.168.0.")); - assert!(!is_ipv4_str("192.168..1")); - } - - #[test] - fn test_hostname_port() { - assert!(!is_domain_port_str("a:12")); - assert!(!is_domain_port_str("a.b.c:12")); - assert!(is_domain_port_str("test.com:12")); - assert!(is_domain_port_str("test-UPPER.com:12")); - assert!(is_domain_port_str("some-other.domain.com:12")); - assert!(!is_domain_port_str("under_score:12")); - assert!(!is_domain_port_str("a@bc:12")); - assert!(!is_domain_port_str("1.1.1.1:12")); - assert!(!is_domain_port_str("1.2.3:12")); - assert!(!is_domain_port_str("1.2.3.45:12")); - assert!(!is_domain_port_str("a.b.c:123456")); - assert!(!is_domain_port_str("---:12")); - assert!(!is_domain_port_str(".:12")); - // todo: should we also check for these edge cases? - // out-of-range port - assert!(is_domain_port_str("test.com:0")); - assert!(is_domain_port_str("test.com:98989")); - } - - #[test] - fn test_mangle2() { - let addr = "[::ffff:127.0.0.1]:8080".parse().unwrap(); - let addr_v4 = "127.0.0.1:8080".parse().unwrap(); - assert_eq!(AddrMangle::decode(&AddrMangle::encode(addr)), addr_v4); - assert_eq!( - AddrMangle::decode(&AddrMangle::encode("[::127.0.0.1]:8080".parse().unwrap())), - addr_v4 - ); - assert_eq!(AddrMangle::decode(&AddrMangle::encode(addr_v4)), addr_v4); - let addr_v6 = "[ef::fe]:8080".parse().unwrap(); - assert_eq!(AddrMangle::decode(&AddrMangle::encode(addr_v6)), addr_v6); - let addr_v6 = "[::1]:8080".parse().unwrap(); - assert_eq!(AddrMangle::decode(&AddrMangle::encode(addr_v6)), addr_v6); - } - - #[test] - fn test_get_version_number() { - assert_eq!(get_version_number("1.1.10"), 1001100); - assert_eq!(get_version_number("1.1.10-1"), 1001101); - assert_eq!(get_version_number("1.1.11-1"), 1001111); - assert_eq!(get_version_number("1.2.3"), 1002030); - } -} diff --git a/libs/hbb_common/src/mem.rs b/libs/hbb_common/src/mem.rs deleted file mode 100644 index 90a5d6d402ed..000000000000 --- a/libs/hbb_common/src/mem.rs +++ /dev/null @@ -1,14 +0,0 @@ -/// SAFETY: the returned Vec must not be resized or reserverd -pub unsafe fn aligned_u8_vec(cap: usize, align: usize) -> Vec { - use std::alloc::*; - - let layout = - Layout::from_size_align(cap, align).expect("invalid aligned value, must be power of 2"); - unsafe { - let ptr = alloc(layout); - if ptr.is_null() { - panic!("failed to allocate {} bytes", cap); - } - Vec::from_raw_parts(ptr, 0, cap) - } -} diff --git a/libs/hbb_common/src/password_security.rs b/libs/hbb_common/src/password_security.rs deleted file mode 100644 index 5c04cc97b928..000000000000 --- a/libs/hbb_common/src/password_security.rs +++ /dev/null @@ -1,295 +0,0 @@ -use crate::config::Config; -use sodiumoxide::base64; -use std::sync::{Arc, RwLock}; - -lazy_static::lazy_static! { - pub static ref TEMPORARY_PASSWORD:Arc> = Arc::new(RwLock::new(Config::get_auto_password(temporary_password_length()))); -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum VerificationMethod { - OnlyUseTemporaryPassword, - OnlyUsePermanentPassword, - UseBothPasswords, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum ApproveMode { - Both, - Password, - Click, -} - -// Should only be called in server -pub fn update_temporary_password() { - *TEMPORARY_PASSWORD.write().unwrap() = Config::get_auto_password(temporary_password_length()); -} - -// Should only be called in server -pub fn temporary_password() -> String { - TEMPORARY_PASSWORD.read().unwrap().clone() -} - -fn verification_method() -> VerificationMethod { - let method = Config::get_option("verification-method"); - if method == "use-temporary-password" { - VerificationMethod::OnlyUseTemporaryPassword - } else if method == "use-permanent-password" { - VerificationMethod::OnlyUsePermanentPassword - } else { - VerificationMethod::UseBothPasswords // default - } -} - -pub fn temporary_password_length() -> usize { - let length = Config::get_option("temporary-password-length"); - if length == "8" { - 8 - } else if length == "10" { - 10 - } else { - 6 // default - } -} - -pub fn temporary_enabled() -> bool { - verification_method() != VerificationMethod::OnlyUsePermanentPassword -} - -pub fn permanent_enabled() -> bool { - verification_method() != VerificationMethod::OnlyUseTemporaryPassword -} - -pub fn has_valid_password() -> bool { - temporary_enabled() && !temporary_password().is_empty() - || permanent_enabled() && !Config::get_permanent_password().is_empty() -} - -pub fn approve_mode() -> ApproveMode { - let mode = Config::get_option("approve-mode"); - if mode == "password" { - ApproveMode::Password - } else if mode == "click" { - ApproveMode::Click - } else { - ApproveMode::Both - } -} - -pub fn hide_cm() -> bool { - approve_mode() == ApproveMode::Password - && verification_method() == VerificationMethod::OnlyUsePermanentPassword - && crate::config::option2bool("allow-hide-cm", &Config::get_option("allow-hide-cm")) -} - -const VERSION_LEN: usize = 2; - -pub fn encrypt_str_or_original(s: &str, version: &str, max_len: usize) -> String { - if decrypt_str_or_original(s, version).1 { - log::error!("Duplicate encryption!"); - return s.to_owned(); - } - if s.chars().count() > max_len { - return String::default(); - } - if version == "00" { - if let Ok(s) = encrypt(s.as_bytes()) { - return version.to_owned() + &s; - } - } - s.to_owned() -} - -// String: password -// bool: whether decryption is successful -// bool: whether should store to re-encrypt when load -// note: s.len() return length in bytes, s.chars().count() return char count -// &[..2] return the left 2 bytes, s.chars().take(2) return the left 2 chars -pub fn decrypt_str_or_original(s: &str, current_version: &str) -> (String, bool, bool) { - if s.len() > VERSION_LEN { - if s.starts_with("00") { - if let Ok(v) = decrypt(s[VERSION_LEN..].as_bytes()) { - return ( - String::from_utf8_lossy(&v).to_string(), - true, - "00" != current_version, - ); - } - } - } - - (s.to_owned(), false, !s.is_empty()) -} - -pub fn encrypt_vec_or_original(v: &[u8], version: &str, max_len: usize) -> Vec { - if decrypt_vec_or_original(v, version).1 { - log::error!("Duplicate encryption!"); - return v.to_owned(); - } - if v.len() > max_len { - return vec![]; - } - if version == "00" { - if let Ok(s) = encrypt(v) { - let mut version = version.to_owned().into_bytes(); - version.append(&mut s.into_bytes()); - return version; - } - } - v.to_owned() -} - -// Vec: password -// bool: whether decryption is successful -// bool: whether should store to re-encrypt when load -pub fn decrypt_vec_or_original(v: &[u8], current_version: &str) -> (Vec, bool, bool) { - if v.len() > VERSION_LEN { - let version = String::from_utf8_lossy(&v[..VERSION_LEN]); - if version == "00" { - if let Ok(v) = decrypt(&v[VERSION_LEN..]) { - return (v, true, version != current_version); - } - } - } - - (v.to_owned(), false, !v.is_empty()) -} - -fn encrypt(v: &[u8]) -> Result { - if !v.is_empty() { - symmetric_crypt(v, true).map(|v| base64::encode(v, base64::Variant::Original)) - } else { - Err(()) - } -} - -fn decrypt(v: &[u8]) -> Result, ()> { - if !v.is_empty() { - base64::decode(v, base64::Variant::Original).and_then(|v| symmetric_crypt(&v, false)) - } else { - Err(()) - } -} - -pub fn symmetric_crypt(data: &[u8], encrypt: bool) -> Result, ()> { - use sodiumoxide::crypto::secretbox; - use std::convert::TryInto; - - let mut keybuf = crate::get_uuid(); - keybuf.resize(secretbox::KEYBYTES, 0); - let key = secretbox::Key(keybuf.try_into().map_err(|_| ())?); - let nonce = secretbox::Nonce([0; secretbox::NONCEBYTES]); - - if encrypt { - Ok(secretbox::seal(data, &nonce, &key)) - } else { - secretbox::open(data, &nonce, &key) - } -} - -mod test { - - #[test] - fn test() { - use super::*; - use rand::{thread_rng, Rng}; - use std::time::Instant; - - let version = "00"; - let max_len = 128; - - println!("test str"); - let data = "1ü1111"; - let encrypted = encrypt_str_or_original(data, version, max_len); - let (decrypted, succ, store) = decrypt_str_or_original(&encrypted, version); - println!("data: {data}"); - println!("encrypted: {encrypted}"); - println!("decrypted: {decrypted}"); - assert_eq!(data, decrypted); - assert_eq!(version, &encrypted[..2]); - assert!(succ); - assert!(!store); - let (_, _, store) = decrypt_str_or_original(&encrypted, "99"); - assert!(store); - assert!(!decrypt_str_or_original(&decrypted, version).1); - assert_eq!( - encrypt_str_or_original(&encrypted, version, max_len), - encrypted - ); - - println!("test vec"); - let data: Vec = "1ü1111".as_bytes().to_vec(); - let encrypted = encrypt_vec_or_original(&data, version, max_len); - let (decrypted, succ, store) = decrypt_vec_or_original(&encrypted, version); - println!("data: {data:?}"); - println!("encrypted: {encrypted:?}"); - println!("decrypted: {decrypted:?}"); - assert_eq!(data, decrypted); - assert_eq!(version.as_bytes(), &encrypted[..2]); - assert!(!store); - assert!(succ); - let (_, _, store) = decrypt_vec_or_original(&encrypted, "99"); - assert!(store); - assert!(!decrypt_vec_or_original(&decrypted, version).1); - assert_eq!( - encrypt_vec_or_original(&encrypted, version, max_len), - encrypted - ); - - println!("test original"); - let data = version.to_string() + "Hello World"; - let (decrypted, succ, store) = decrypt_str_or_original(&data, version); - assert_eq!(data, decrypted); - assert!(store); - assert!(!succ); - let verbytes = version.as_bytes(); - let data: Vec = vec![verbytes[0], verbytes[1], 1, 2, 3, 4, 5, 6]; - let (decrypted, succ, store) = decrypt_vec_or_original(&data, version); - assert_eq!(data, decrypted); - assert!(store); - assert!(!succ); - let (_, succ, store) = decrypt_str_or_original("", version); - assert!(!store); - assert!(!succ); - let (_, succ, store) = decrypt_vec_or_original(&[], version); - assert!(!store); - assert!(!succ); - let data = "1ü1111"; - assert_eq!(decrypt_str_or_original(data, version).0, data); - let data: Vec = "1ü1111".as_bytes().to_vec(); - assert_eq!(decrypt_vec_or_original(&data, version).0, data); - - println!("test speed"); - let test_speed = |len: usize, name: &str| { - let mut data: Vec = vec![]; - let mut rng = thread_rng(); - for _ in 0..len { - data.push(rng.gen_range(0..255)); - } - let start: Instant = Instant::now(); - let encrypted = encrypt_vec_or_original(&data, version, len); - assert_ne!(data, decrypted); - let t1 = start.elapsed(); - let start = Instant::now(); - let (decrypted, _, _) = decrypt_vec_or_original(&encrypted, version); - let t2 = start.elapsed(); - assert_eq!(data, decrypted); - println!("{name}"); - println!("encrypt:{:?}, decrypt:{:?}", t1, t2); - - let start: Instant = Instant::now(); - let encrypted = base64::encode(&data, base64::Variant::Original); - let t1 = start.elapsed(); - let start = Instant::now(); - let decrypted = base64::decode(&encrypted, base64::Variant::Original).unwrap(); - let t2 = start.elapsed(); - assert_eq!(data, decrypted); - println!("base64, encrypt:{:?}, decrypt:{:?}", t1, t2,); - }; - test_speed(128, "128"); - test_speed(1024, "1k"); - test_speed(1024 * 1024, "1M"); - test_speed(10 * 1024 * 1024, "10M"); - test_speed(100 * 1024 * 1024, "100M"); - } -} diff --git a/libs/hbb_common/src/platform/linux.rs b/libs/hbb_common/src/platform/linux.rs deleted file mode 100644 index 60c8714d8212..000000000000 --- a/libs/hbb_common/src/platform/linux.rs +++ /dev/null @@ -1,300 +0,0 @@ -use crate::ResultType; -use std::{collections::HashMap, process::Command}; - -lazy_static::lazy_static! { - pub static ref DISTRO: Distro = Distro::new(); -} - -pub const DISPLAY_SERVER_WAYLAND: &str = "wayland"; -pub const DISPLAY_SERVER_X11: &str = "x11"; -pub const DISPLAY_DESKTOP_KDE: &str = "KDE"; - -pub const XDG_CURRENT_DESKTOP: &str = "XDG_CURRENT_DESKTOP"; - -pub struct Distro { - pub name: String, - pub version_id: String, -} - -impl Distro { - fn new() -> Self { - let name = run_cmds("awk -F'=' '/^NAME=/ {print $2}' /etc/os-release") - .unwrap_or_default() - .trim() - .trim_matches('"') - .to_string(); - let version_id = run_cmds("awk -F'=' '/^VERSION_ID=/ {print $2}' /etc/os-release") - .unwrap_or_default() - .trim() - .trim_matches('"') - .to_string(); - Self { name, version_id } - } -} - -#[inline] -pub fn is_kde() -> bool { - if let Ok(env) = std::env::var(XDG_CURRENT_DESKTOP) { - env == DISPLAY_DESKTOP_KDE - } else { - false - } -} - -#[inline] -pub fn is_gdm_user(username: &str) -> bool { - username == "gdm" - // || username == "lightgdm" -} - -#[inline] -pub fn is_desktop_wayland() -> bool { - get_display_server() == DISPLAY_SERVER_WAYLAND -} - -#[inline] -pub fn is_x11_or_headless() -> bool { - !is_desktop_wayland() -} - -// -1 -const INVALID_SESSION: &str = "4294967295"; - -pub fn get_display_server() -> String { - // Check for forced display server environment variable first - if let Ok(forced_display) = std::env::var("RUSTDESK_FORCED_DISPLAY_SERVER") { - return forced_display; - } - - // Check if `loginctl` can be called successfully - if run_loginctl(None).is_err() { - return DISPLAY_SERVER_X11.to_owned(); - } - - let mut session = get_values_of_seat0(&[0])[0].clone(); - if session.is_empty() { - // loginctl has not given the expected output. try something else. - if let Ok(sid) = std::env::var("XDG_SESSION_ID") { - // could also execute "cat /proc/self/sessionid" - session = sid; - } - if session.is_empty() { - session = run_cmds("cat /proc/self/sessionid").unwrap_or_default(); - if session == INVALID_SESSION { - session = "".to_owned(); - } - } - } - if session.is_empty() { - std::env::var("XDG_SESSION_TYPE").unwrap_or("x11".to_owned()) - } else { - get_display_server_of_session(&session) - } -} - -pub fn get_display_server_of_session(session: &str) -> String { - let mut display_server = if let Ok(output) = - run_loginctl(Some(vec!["show-session", "-p", "Type", session])) - // Check session type of the session - { - String::from_utf8_lossy(&output.stdout) - .replace("Type=", "") - .trim_end() - .into() - } else { - "".to_owned() - }; - if display_server.is_empty() || display_server == "tty" { - if let Ok(sestype) = std::env::var("XDG_SESSION_TYPE") { - if !sestype.is_empty() { - return sestype.to_lowercase(); - } - } - display_server = "x11".to_owned(); - } - display_server.to_lowercase() -} - -#[inline] -fn line_values(indices: &[usize], line: &str) -> Vec { - indices - .into_iter() - .map(|idx| line.split_whitespace().nth(*idx).unwrap_or("").to_owned()) - .collect::>() -} - -#[inline] -pub fn get_values_of_seat0(indices: &[usize]) -> Vec { - _get_values_of_seat0(indices, true) -} - -#[inline] -pub fn get_values_of_seat0_with_gdm_wayland(indices: &[usize]) -> Vec { - _get_values_of_seat0(indices, false) -} - -// Ignore "3 sessions listed." -fn ignore_loginctl_line(line: &str) -> bool { - line.contains("sessions") || line.split(" ").count() < 4 -} - -fn _get_values_of_seat0(indices: &[usize], ignore_gdm_wayland: bool) -> Vec { - if let Ok(output) = run_loginctl(None) { - for line in String::from_utf8_lossy(&output.stdout).lines() { - if ignore_loginctl_line(line) { - continue; - } - if line.contains("seat0") { - if let Some(sid) = line.split_whitespace().next() { - if is_active(sid) { - if ignore_gdm_wayland { - if is_gdm_user(line.split_whitespace().nth(2).unwrap_or("")) - && get_display_server_of_session(sid) == DISPLAY_SERVER_WAYLAND - { - continue; - } - } - return line_values(indices, line); - } - } - } - } - - // some case, there is no seat0 https://github.com/rustdesk/rustdesk/issues/73 - for line in String::from_utf8_lossy(&output.stdout).lines() { - if ignore_loginctl_line(line) { - continue; - } - if let Some(sid) = line.split_whitespace().next() { - if is_active(sid) { - let d = get_display_server_of_session(sid); - if ignore_gdm_wayland { - if is_gdm_user(line.split_whitespace().nth(2).unwrap_or("")) - && d == DISPLAY_SERVER_WAYLAND - { - continue; - } - } - if d == "tty" { - continue; - } - return line_values(indices, line); - } - } - } - } - - line_values(indices, "") -} - -pub fn is_active(sid: &str) -> bool { - if let Ok(output) = run_loginctl(Some(vec!["show-session", "-p", "State", sid])) { - String::from_utf8_lossy(&output.stdout).contains("active") - } else { - false - } -} - -pub fn is_active_and_seat0(sid: &str) -> bool { - if let Ok(output) = run_loginctl(Some(vec!["show-session", sid])) { - String::from_utf8_lossy(&output.stdout).contains("State=active") - && String::from_utf8_lossy(&output.stdout).contains("Seat=seat0") - } else { - false - } -} - -// **Note** that the return value here, the last character is '\n'. -// Use `run_cmds_trim_newline()` if you want to remove '\n' at the end. -pub fn run_cmds(cmds: &str) -> ResultType { - let output = std::process::Command::new("sh") - .args(vec!["-c", cmds]) - .output()?; - Ok(String::from_utf8_lossy(&output.stdout).to_string()) -} - -pub fn run_cmds_trim_newline(cmds: &str) -> ResultType { - let output = std::process::Command::new("sh") - .args(vec!["-c", cmds]) - .output()?; - let out = String::from_utf8_lossy(&output.stdout); - Ok(if out.ends_with('\n') { - out[..out.len() - 1].to_string() - } else { - out.to_string() - }) -} - -fn run_loginctl(args: Option>) -> std::io::Result { - if std::env::var("FLATPAK_ID").is_ok() { - let mut l_args = String::from("loginctl"); - if let Some(a) = args.as_ref() { - l_args = format!("{} {}", l_args, a.join(" ")); - } - let res = std::process::Command::new("flatpak-spawn") - .args(vec![String::from("--host"), l_args]) - .output(); - if res.is_ok() { - return res; - } - } - let mut cmd = std::process::Command::new("loginctl"); - if let Some(a) = args { - return cmd.args(a).output(); - } - cmd.output() -} - -/// forever: may not work -#[cfg(target_os = "linux")] -pub fn system_message(title: &str, msg: &str, forever: bool) -> ResultType<()> { - let cmds: HashMap<&str, Vec<&str>> = HashMap::from([ - ("notify-send", [title, msg].to_vec()), - ( - "zenity", - [ - "--info", - "--timeout", - if forever { "0" } else { "3" }, - "--title", - title, - "--text", - msg, - ] - .to_vec(), - ), - ("kdialog", ["--title", title, "--msgbox", msg].to_vec()), - ( - "xmessage", - [ - "-center", - "-timeout", - if forever { "0" } else { "3" }, - title, - msg, - ] - .to_vec(), - ), - ]); - for (k, v) in cmds { - if Command::new(k).args(v).spawn().is_ok() { - return Ok(()); - } - } - crate::bail!("failed to post system message"); -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_run_cmds_trim_newline() { - assert_eq!(run_cmds_trim_newline("echo -n 123").unwrap(), "123"); - assert_eq!(run_cmds_trim_newline("echo 123").unwrap(), "123"); - assert_eq!( - run_cmds_trim_newline("whoami").unwrap() + "\n", - run_cmds("whoami").unwrap() - ); - } -} diff --git a/libs/hbb_common/src/platform/macos.rs b/libs/hbb_common/src/platform/macos.rs deleted file mode 100644 index dd83a87385b9..000000000000 --- a/libs/hbb_common/src/platform/macos.rs +++ /dev/null @@ -1,55 +0,0 @@ -use crate::ResultType; -use osascript; -use serde_derive::{Deserialize, Serialize}; - -#[derive(Serialize)] -struct AlertParams { - title: String, - message: String, - alert_type: String, - buttons: Vec, -} - -#[derive(Deserialize)] -struct AlertResult { - #[serde(rename = "buttonReturned")] - button: String, -} - -/// Firstly run the specified app, then alert a dialog. Return the clicked button value. -/// -/// # Arguments -/// -/// * `app` - The app to execute the script. -/// * `alert_type` - Alert type. . informational, warning, critical -/// * `title` - The alert title. -/// * `message` - The alert message. -/// * `buttons` - The buttons to show. -pub fn alert( - app: String, - alert_type: String, - title: String, - message: String, - buttons: Vec, -) -> ResultType { - let script = osascript::JavaScript::new(&format!( - " - var App = Application('{}'); - App.includeStandardAdditions = true; - return App.displayAlert($params.title, {{ - message: $params.message, - 'as': $params.alert_type, - buttons: $params.buttons, - }}); - ", - app - )); - - let result: AlertResult = script.execute_with_params(AlertParams { - title, - message, - alert_type, - buttons, - })?; - Ok(result.button) -} diff --git a/libs/hbb_common/src/platform/mod.rs b/libs/hbb_common/src/platform/mod.rs deleted file mode 100644 index 5dc004a81b7f..000000000000 --- a/libs/hbb_common/src/platform/mod.rs +++ /dev/null @@ -1,81 +0,0 @@ -#[cfg(target_os = "linux")] -pub mod linux; - -#[cfg(target_os = "macos")] -pub mod macos; - -#[cfg(target_os = "windows")] -pub mod windows; - -#[cfg(not(debug_assertions))] -use crate::{config::Config, log}; -#[cfg(not(debug_assertions))] -use std::process::exit; - -#[cfg(not(debug_assertions))] -static mut GLOBAL_CALLBACK: Option> = None; - -#[cfg(not(debug_assertions))] -extern "C" fn breakdown_signal_handler(sig: i32) { - let mut stack = vec![]; - backtrace::trace(|frame| { - backtrace::resolve_frame(frame, |symbol| { - if let Some(name) = symbol.name() { - stack.push(name.to_string()); - } - }); - true // keep going to the next frame - }); - let mut info = String::default(); - if stack.iter().any(|s| { - s.contains(&"nouveau_pushbuf_kick") - || s.to_lowercase().contains("nvidia") - || s.contains("gdk_window_end_draw_frame") - || s.contains("glGetString") - }) { - Config::set_option("allow-always-software-render".to_string(), "Y".to_string()); - info = "Always use software rendering will be set.".to_string(); - log::info!("{}", info); - } - if stack.iter().any(|s| { - s.to_lowercase().contains("nvidia") - || s.to_lowercase().contains("amf") - || s.to_lowercase().contains("mfx") - || s.contains("cuProfilerStop") - }) { - Config::set_option("enable-hwcodec".to_string(), "N".to_string()); - info = "Perhaps hwcodec causing the crash, disable it first".to_string(); - log::info!("{}", info); - } - log::error!( - "Got signal {} and exit. stack:\n{}", - sig, - stack.join("\n").to_string() - ); - if !info.is_empty() { - #[cfg(target_os = "linux")] - linux::system_message( - "RustDesk", - &format!("Got signal {} and exit.{}", sig, info), - true, - ) - .ok(); - } - unsafe { - if let Some(callback) = &GLOBAL_CALLBACK { - callback() - } - } - exit(0); -} - -#[cfg(not(debug_assertions))] -pub fn register_breakdown_handler(callback: T) -where - T: Fn() + 'static, -{ - unsafe { - GLOBAL_CALLBACK = Some(Box::new(callback)); - libc::signal(libc::SIGSEGV, breakdown_signal_handler as _); - } -} diff --git a/libs/hbb_common/src/platform/windows.rs b/libs/hbb_common/src/platform/windows.rs deleted file mode 100644 index 7481631ace17..000000000000 --- a/libs/hbb_common/src/platform/windows.rs +++ /dev/null @@ -1,198 +0,0 @@ -use std::{ - collections::VecDeque, - sync::{Arc, Mutex}, - time::Instant, -}; -use winapi::{ - shared::minwindef::{DWORD, FALSE, TRUE}, - um::{ - handleapi::CloseHandle, - pdh::{ - PdhAddEnglishCounterA, PdhCloseQuery, PdhCollectQueryData, PdhCollectQueryDataEx, - PdhGetFormattedCounterValue, PdhOpenQueryA, PDH_FMT_COUNTERVALUE, PDH_FMT_DOUBLE, - PDH_HCOUNTER, PDH_HQUERY, - }, - synchapi::{CreateEventA, WaitForSingleObject}, - sysinfoapi::VerSetConditionMask, - winbase::{VerifyVersionInfoW, INFINITE, WAIT_OBJECT_0}, - winnt::{ - HANDLE, OSVERSIONINFOEXW, VER_BUILDNUMBER, VER_GREATER_EQUAL, VER_MAJORVERSION, - VER_MINORVERSION, VER_SERVICEPACKMAJOR, VER_SERVICEPACKMINOR, - }, - }, -}; - -lazy_static::lazy_static! { - static ref CPU_USAGE_ONE_MINUTE: Arc>> = Arc::new(Mutex::new(None)); -} - -// https://github.com/mgostIH/process_list/blob/master/src/windows/mod.rs -#[repr(transparent)] -pub struct RAIIHandle(pub HANDLE); - -impl Drop for RAIIHandle { - fn drop(&mut self) { - // This never gives problem except when running under a debugger. - unsafe { CloseHandle(self.0) }; - } -} - -#[repr(transparent)] -pub(self) struct RAIIPDHQuery(pub PDH_HQUERY); - -impl Drop for RAIIPDHQuery { - fn drop(&mut self) { - unsafe { PdhCloseQuery(self.0) }; - } -} - -pub fn start_cpu_performance_monitor() { - // Code from: - // https://learn.microsoft.com/en-us/windows/win32/perfctrs/collecting-performance-data - // https://learn.microsoft.com/en-us/windows/win32/api/pdh/nf-pdh-pdhcollectquerydataex - // Why value lower than taskManager: - // https://aaron-margosis.medium.com/task-managers-cpu-numbers-are-all-but-meaningless-2d165b421e43 - // Therefore we should compare with Precess Explorer rather than taskManager - - let f = || unsafe { - // load avg or cpu usage, test with prime95. - // Prefer cpu usage because we can get accurate value from Precess Explorer. - // const COUNTER_PATH: &'static str = "\\System\\Processor Queue Length\0"; - const COUNTER_PATH: &'static str = "\\Processor(_total)\\% Processor Time\0"; - const SAMPLE_INTERVAL: DWORD = 2; // 2 second - - let mut ret; - let mut query: PDH_HQUERY = std::mem::zeroed(); - ret = PdhOpenQueryA(std::ptr::null() as _, 0, &mut query); - if ret != 0 { - log::error!("PdhOpenQueryA failed: 0x{:X}", ret); - return; - } - let _query = RAIIPDHQuery(query); - let mut counter: PDH_HCOUNTER = std::mem::zeroed(); - ret = PdhAddEnglishCounterA(query, COUNTER_PATH.as_ptr() as _, 0, &mut counter); - if ret != 0 { - log::error!("PdhAddEnglishCounterA failed: 0x{:X}", ret); - return; - } - ret = PdhCollectQueryData(query); - if ret != 0 { - log::error!("PdhCollectQueryData failed: 0x{:X}", ret); - return; - } - let mut _counter_type: DWORD = 0; - let mut counter_value: PDH_FMT_COUNTERVALUE = std::mem::zeroed(); - let event = CreateEventA(std::ptr::null_mut(), FALSE, FALSE, std::ptr::null() as _); - if event.is_null() { - log::error!("CreateEventA failed"); - return; - } - let _event: RAIIHandle = RAIIHandle(event); - ret = PdhCollectQueryDataEx(query, SAMPLE_INTERVAL, event); - if ret != 0 { - log::error!("PdhCollectQueryDataEx failed: 0x{:X}", ret); - return; - } - - let mut queue: VecDeque = VecDeque::new(); - let mut recent_valid: VecDeque = VecDeque::new(); - loop { - // latest one minute - if queue.len() == 31 { - queue.pop_front(); - } - if recent_valid.len() == 31 { - recent_valid.pop_front(); - } - // allow get value within one minute - if queue.len() > 0 && recent_valid.iter().filter(|v| **v).count() > queue.len() / 2 { - let sum: f64 = queue.iter().map(|f| f.to_owned()).sum(); - let avg = sum / (queue.len() as f64); - *CPU_USAGE_ONE_MINUTE.lock().unwrap() = Some((avg, Instant::now())); - } else { - *CPU_USAGE_ONE_MINUTE.lock().unwrap() = None; - } - if WAIT_OBJECT_0 != WaitForSingleObject(event, INFINITE) { - recent_valid.push_back(false); - continue; - } - if PdhGetFormattedCounterValue( - counter, - PDH_FMT_DOUBLE, - &mut _counter_type, - &mut counter_value, - ) != 0 - || counter_value.CStatus != 0 - { - recent_valid.push_back(false); - continue; - } - queue.push_back(counter_value.u.doubleValue().clone()); - recent_valid.push_back(true); - } - }; - use std::sync::Once; - static ONCE: Once = Once::new(); - ONCE.call_once(|| { - std::thread::spawn(f); - }); -} - -pub fn cpu_uage_one_minute() -> Option { - let v = CPU_USAGE_ONE_MINUTE.lock().unwrap().clone(); - if let Some((v, instant)) = v { - if instant.elapsed().as_secs() < 30 { - return Some(v); - } - } - None -} - -pub fn sync_cpu_usage(cpu_usage: Option) { - let v = match cpu_usage { - Some(cpu_usage) => Some((cpu_usage, Instant::now())), - None => None, - }; - *CPU_USAGE_ONE_MINUTE.lock().unwrap() = v; - log::info!("cpu usage synced: {:?}", cpu_usage); -} - -// https://learn.microsoft.com/en-us/windows/win32/sysinfo/targeting-your-application-at-windows-8-1 -// https://github.com/nodejs/node-convergence-archive/blob/e11fe0c2777561827cdb7207d46b0917ef3c42a7/deps/uv/src/win/util.c#L780 -pub fn is_windows_version_or_greater( - os_major: u32, - os_minor: u32, - build_number: u32, - service_pack_major: u32, - service_pack_minor: u32, -) -> bool { - let mut osvi: OSVERSIONINFOEXW = unsafe { std::mem::zeroed() }; - osvi.dwOSVersionInfoSize = std::mem::size_of::() as DWORD; - osvi.dwMajorVersion = os_major as _; - osvi.dwMinorVersion = os_minor as _; - osvi.dwBuildNumber = build_number as _; - osvi.wServicePackMajor = service_pack_major as _; - osvi.wServicePackMinor = service_pack_minor as _; - - let result = unsafe { - let mut condition_mask = 0; - let op = VER_GREATER_EQUAL; - condition_mask = VerSetConditionMask(condition_mask, VER_MAJORVERSION, op); - condition_mask = VerSetConditionMask(condition_mask, VER_MINORVERSION, op); - condition_mask = VerSetConditionMask(condition_mask, VER_BUILDNUMBER, op); - condition_mask = VerSetConditionMask(condition_mask, VER_SERVICEPACKMAJOR, op); - condition_mask = VerSetConditionMask(condition_mask, VER_SERVICEPACKMINOR, op); - - VerifyVersionInfoW( - &mut osvi as *mut OSVERSIONINFOEXW, - VER_MAJORVERSION - | VER_MINORVERSION - | VER_BUILDNUMBER - | VER_SERVICEPACKMAJOR - | VER_SERVICEPACKMINOR, - condition_mask, - ) - }; - - result == TRUE -} diff --git a/libs/hbb_common/src/protos/mod.rs b/libs/hbb_common/src/protos/mod.rs deleted file mode 100644 index 57d9b68fe341..000000000000 --- a/libs/hbb_common/src/protos/mod.rs +++ /dev/null @@ -1 +0,0 @@ -include!(concat!(env!("OUT_DIR"), "/protos/mod.rs")); diff --git a/libs/hbb_common/src/proxy.rs b/libs/hbb_common/src/proxy.rs deleted file mode 100644 index 34d2c5109f5a..000000000000 --- a/libs/hbb_common/src/proxy.rs +++ /dev/null @@ -1,561 +0,0 @@ -use std::{ - io::Error as IoError, - net::{SocketAddr, ToSocketAddrs}, -}; - -use base64::{engine::general_purpose, Engine}; -use httparse::{Error as HttpParseError, Response, EMPTY_HEADER}; -use log::info; -use thiserror::Error as ThisError; -use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt, BufStream}; -#[cfg(any(target_os = "windows", target_os = "macos"))] -use tokio_native_tls::{native_tls, TlsConnector, TlsStream}; -#[cfg(not(any(target_os = "windows", target_os = "macos")))] -use tokio_rustls::{client::TlsStream, TlsConnector}; -use tokio_socks::{tcp::Socks5Stream, IntoTargetAddr}; -use tokio_util::codec::Framed; -use url::Url; - -use crate::{ - bytes_codec::BytesCodec, - config::Socks5Server, - tcp::{DynTcpStream, FramedStream}, - ResultType, -}; - -#[derive(Debug, ThisError)] -pub enum ProxyError { - #[error("IO Error: {0}")] - IoError(#[from] IoError), - #[error("Target parse error: {0}")] - TargetParseError(String), - #[error("HTTP parse error: {0}")] - HttpParseError(#[from] HttpParseError), - #[error("The maximum response header length is exceeded: {0}")] - MaximumResponseHeaderLengthExceeded(usize), - #[error("The end of file is reached")] - EndOfFile, - #[error("The url is error: {0}")] - UrlBadScheme(String), - #[error("The url parse error: {0}")] - UrlParseScheme(#[from] url::ParseError), - #[error("No HTTP code was found in the response")] - NoHttpCode, - #[error("The HTTP code is not equal 200: {0}")] - HttpCode200(u16), - #[error("The proxy address resolution failed: {0}")] - AddressResolutionFailed(String), - #[cfg(any(target_os = "windows", target_os = "macos"))] - #[error("The native tls error: {0}")] - NativeTlsError(#[from] tokio_native_tls::native_tls::Error), -} - -const MAXIMUM_RESPONSE_HEADER_LENGTH: usize = 4096; -/// The maximum HTTP Headers, which can be parsed. -const MAXIMUM_RESPONSE_HEADERS: usize = 16; -const DEFINE_TIME_OUT: u64 = 600; - -pub trait IntoUrl { - - // Besides parsing as a valid `Url`, the `Url` must be a valid - // `http::Uri`, in that it makes sense to use in a network request. - fn into_url(self) -> Result; - - fn as_str(&self) -> &str; -} - -impl IntoUrl for Url { - fn into_url(self) -> Result { - if self.has_host() { - Ok(self) - } else { - Err(ProxyError::UrlBadScheme(self.to_string())) - } - } - - fn as_str(&self) -> &str { - self.as_ref() - } -} - -impl<'a> IntoUrl for &'a str { - fn into_url(self) -> Result { - Url::parse(self) - .map_err(ProxyError::UrlParseScheme)? - .into_url() - } - - fn as_str(&self) -> &str { - self - } -} - -impl<'a> IntoUrl for &'a String { - fn into_url(self) -> Result { - (&**self).into_url() - } - - fn as_str(&self) -> &str { - self.as_ref() - } -} - -impl<'a> IntoUrl for String { - fn into_url(self) -> Result { - (&*self).into_url() - } - - fn as_str(&self) -> &str { - self.as_ref() - } -} - -#[derive(Clone)] -pub struct Auth { - user_name: String, - password: String, -} - -impl Auth { - fn get_proxy_authorization(&self) -> String { - format!( - "Proxy-Authorization: Basic {}\r\n", - self.get_basic_authorization() - ) - } - - pub fn get_basic_authorization(&self) -> String { - let authorization = format!("{}:{}", &self.user_name, &self.password); - general_purpose::STANDARD.encode(authorization.as_bytes()) - } -} - -#[derive(Clone)] -pub enum ProxyScheme { - Http { - auth: Option, - host: String, - }, - Https { - auth: Option, - host: String, - }, - Socks5 { - addr: SocketAddr, - auth: Option, - remote_dns: bool, - }, -} - -impl ProxyScheme { - pub fn maybe_auth(&self) -> Option<&Auth> { - match self { - ProxyScheme::Http { auth, .. } - | ProxyScheme::Https { auth, .. } - | ProxyScheme::Socks5 { auth, .. } => auth.as_ref(), - } - } - - fn socks5(addr: SocketAddr) -> Result { - Ok(ProxyScheme::Socks5 { - addr, - auth: None, - remote_dns: false, - }) - } - - fn http(host: &str) -> Result { - Ok(ProxyScheme::Http { - auth: None, - host: host.to_string(), - }) - } - fn https(host: &str) -> Result { - Ok(ProxyScheme::Https { - auth: None, - host: host.to_string(), - }) - } - - fn set_basic_auth, U: Into>(&mut self, username: T, password: U) { - let auth = Auth { - user_name: username.into(), - password: password.into(), - }; - match self { - ProxyScheme::Http { auth: a, .. } => *a = Some(auth), - ProxyScheme::Https { auth: a, .. } => *a = Some(auth), - ProxyScheme::Socks5 { auth: a, .. } => *a = Some(auth), - } - } - - fn parse(url: Url) -> Result { - use url::Position; - - // Resolve URL to a host and port - let to_addr = || { - let addrs = url.socket_addrs(|| match url.scheme() { - "socks5" => Some(1080), - _ => None, - })?; - addrs - .into_iter() - .next() - .ok_or_else(|| ProxyError::UrlParseScheme(url::ParseError::EmptyHost)) - }; - - let mut scheme: Self = match url.scheme() { - "http" => Self::http(&url[Position::BeforeHost..Position::AfterPort])?, - "https" => Self::https(&url[Position::BeforeHost..Position::AfterPort])?, - "socks5" => Self::socks5(to_addr()?)?, - e => return Err(ProxyError::UrlBadScheme(e.to_string())), - }; - - if let Some(pwd) = url.password() { - let username = url.username(); - scheme.set_basic_auth(username, pwd); - } - - Ok(scheme) - } - pub async fn socket_addrs(&self) -> Result { - info!("Resolving socket address"); - match self { - ProxyScheme::Http { host, .. } => self.resolve_host(host, 80).await, - ProxyScheme::Https { host, .. } => self.resolve_host(host, 443).await, - ProxyScheme::Socks5 { addr, .. } => Ok(addr.clone()), - } - } - - async fn resolve_host(&self, host: &str, default_port: u16) -> Result { - let (host_str, port) = match host.split_once(':') { - Some((h, p)) => (h, p.parse::().ok()), - None => (host, None), - }; - let addr = (host_str, port.unwrap_or(default_port)) - .to_socket_addrs()? - .next() - .ok_or_else(|| ProxyError::AddressResolutionFailed(host.to_string()))?; - Ok(addr) - } - - pub fn get_domain(&self) -> Result { - match self { - ProxyScheme::Http { host, .. } | ProxyScheme::Https { host, .. } => { - let domain = host - .split(':') - .next() - .ok_or_else(|| ProxyError::AddressResolutionFailed(host.clone()))?; - Ok(domain.to_string()) - } - ProxyScheme::Socks5 { addr, .. } => match addr { - SocketAddr::V4(addr_v4) => Ok(addr_v4.ip().to_string()), - SocketAddr::V6(addr_v6) => Ok(addr_v6.ip().to_string()), - }, - } - } - pub fn get_host_and_port(&self) -> Result { - match self { - ProxyScheme::Http { host, .. } => Ok(self.append_default_port(host, 80)), - ProxyScheme::Https { host, .. } => Ok(self.append_default_port(host, 443)), - ProxyScheme::Socks5 { addr, .. } => Ok(format!("{}", addr)), - } - } - fn append_default_port(&self, host: &str, default_port: u16) -> String { - if host.contains(':') { - host.to_string() - } else { - format!("{}:{}", host, default_port) - } - } -} - -pub trait IntoProxyScheme { - fn into_proxy_scheme(self) -> Result; -} - -impl IntoProxyScheme for S { - fn into_proxy_scheme(self) -> Result { - // validate the URL - let url = match self.as_str().into_url() { - Ok(ok) => ok, - Err(e) => { - match e { - // If the string does not contain protocol headers, try to parse it using the socks5 protocol - ProxyError::UrlParseScheme(_source) => { - let try_this = format!("socks5://{}", self.as_str()); - try_this.into_url()? - } - _ => { - return Err(e); - } - } - } - }; - ProxyScheme::parse(url) - } -} - -impl IntoProxyScheme for ProxyScheme { - fn into_proxy_scheme(self) -> Result { - Ok(self) - } -} - -#[derive(Clone)] -pub struct Proxy { - pub intercept: ProxyScheme, - ms_timeout: u64, -} - -impl Proxy { - pub fn new(proxy_scheme: U, ms_timeout: u64) -> Result { - Ok(Self { - intercept: proxy_scheme.into_proxy_scheme()?, - ms_timeout, - }) - } - - pub fn is_http_or_https(&self) -> bool { - return match self.intercept { - ProxyScheme::Socks5 { .. } => false, - _ => true, - }; - } - - pub fn from_conf(conf: &Socks5Server, ms_timeout: Option) -> Result { - let mut proxy; - match ms_timeout { - None => { - proxy = Self::new(&conf.proxy, DEFINE_TIME_OUT)?; - } - Some(time_out) => { - proxy = Self::new(&conf.proxy, time_out)?; - } - } - - if !conf.password.is_empty() && !conf.username.is_empty() { - proxy = proxy.basic_auth(&conf.username, &conf.password); - } - Ok(proxy) - } - - pub async fn proxy_addrs(&self) -> Result { - self.intercept.socket_addrs().await - } - - fn basic_auth(mut self, username: &str, password: &str) -> Proxy { - self.intercept.set_basic_auth(username, password); - self - } - - pub async fn connect<'t, T>( - self, - target: T, - local_addr: Option, - ) -> ResultType - where - T: IntoTargetAddr<'t>, - { - info!("Connect to proxy server"); - let proxy = self.proxy_addrs().await?; - - let local = if let Some(addr) = local_addr { - addr - } else { - crate::config::Config::get_any_listen_addr(proxy.is_ipv4()) - }; - - let stream = super::timeout( - self.ms_timeout, - crate::tcp::new_socket(local, true)?.connect(proxy), - ) - .await??; - stream.set_nodelay(true).ok(); - - let addr = stream.local_addr()?; - - return match self.intercept { - ProxyScheme::Http { .. } => { - info!("Connect to remote http proxy server: {}", proxy); - let stream = - super::timeout(self.ms_timeout, self.http_connect(stream, target)).await??; - Ok(FramedStream( - Framed::new(DynTcpStream(Box::new(stream)), BytesCodec::new()), - addr, - None, - 0, - )) - } - ProxyScheme::Https { .. } => { - info!("Connect to remote https proxy server: {}", proxy); - let stream = - super::timeout(self.ms_timeout, self.https_connect(stream, target)).await??; - Ok(FramedStream( - Framed::new(DynTcpStream(Box::new(stream)), BytesCodec::new()), - addr, - None, - 0, - )) - } - ProxyScheme::Socks5 { .. } => { - info!("Connect to remote socket5 proxy server: {}", proxy); - let stream = if let Some(auth) = self.intercept.maybe_auth() { - super::timeout( - self.ms_timeout, - Socks5Stream::connect_with_password_and_socket( - stream, - target, - &auth.user_name, - &auth.password, - ), - ) - .await?? - } else { - super::timeout( - self.ms_timeout, - Socks5Stream::connect_with_socket(stream, target), - ) - .await?? - }; - Ok(FramedStream( - Framed::new(DynTcpStream(Box::new(stream)), BytesCodec::new()), - addr, - None, - 0, - )) - } - }; - } - - #[cfg(any(target_os = "windows", target_os = "macos"))] - pub async fn https_connect<'a, Input, T>( - self, - io: Input, - target: T, - ) -> Result>, ProxyError> - where - Input: AsyncRead + AsyncWrite + Unpin, - T: IntoTargetAddr<'a>, - { - let tls_connector = TlsConnector::from(native_tls::TlsConnector::new()?); - let stream = tls_connector - .connect(&self.intercept.get_domain()?, io) - .await?; - self.http_connect(stream, target).await - } - - #[cfg(not(any(target_os = "windows", target_os = "macos")))] - pub async fn https_connect<'a, Input, T>( - self, - io: Input, - target: T, - ) -> Result>, ProxyError> - where - Input: AsyncRead + AsyncWrite + Unpin, - T: IntoTargetAddr<'a>, - { - use std::convert::TryFrom; - let verifier = rustls_platform_verifier::tls_config(); - let url_domain = self.intercept.get_domain()?; - - let domain = rustls_pki_types::ServerName::try_from(url_domain.as_str()) - .map_err(|e| ProxyError::AddressResolutionFailed(e.to_string()))? - .to_owned(); - - let tls_connector = TlsConnector::from(std::sync::Arc::new(verifier)); - let stream = tls_connector.connect(domain, io).await?; - self.http_connect(stream, target).await - } - - pub async fn http_connect<'a, Input, T>( - self, - io: Input, - target: T, - ) -> Result, ProxyError> - where - Input: AsyncRead + AsyncWrite + Unpin, - T: IntoTargetAddr<'a>, - { - let mut stream = BufStream::new(io); - let (domain, port) = get_domain_and_port(target)?; - - let request = self.make_request(&domain, port); - stream.write_all(request.as_bytes()).await?; - stream.flush().await?; - recv_and_check_response(&mut stream).await?; - Ok(stream) - } - - fn make_request(&self, host: &str, port: u16) -> String { - let mut request = format!( - "CONNECT {host}:{port} HTTP/1.1\r\nHost: {host}:{port}\r\n", - host = host, - port = port - ); - - if let Some(auth) = self.intercept.maybe_auth() { - request = format!("{}{}", request, auth.get_proxy_authorization()); - } - - request.push_str("\r\n"); - request - } -} - -fn get_domain_and_port<'a, T: IntoTargetAddr<'a>>(target: T) -> Result<(String, u16), ProxyError> { - let target_addr = target - .into_target_addr() - .map_err(|e| ProxyError::TargetParseError(e.to_string()))?; - match target_addr { - tokio_socks::TargetAddr::Ip(addr) => Ok((addr.ip().to_string(), addr.port())), - tokio_socks::TargetAddr::Domain(name, port) => Ok((name.to_string(), port)), - } -} - -async fn get_response(stream: &mut BufStream) -> Result -where - IO: AsyncRead + AsyncWrite + Unpin, -{ - use tokio::io::AsyncBufReadExt; - let mut response = String::new(); - - loop { - if stream.read_line(&mut response).await? == 0 { - return Err(ProxyError::EndOfFile); - } - - if MAXIMUM_RESPONSE_HEADER_LENGTH < response.len() { - return Err(ProxyError::MaximumResponseHeaderLengthExceeded( - response.len(), - )); - } - - if response.ends_with("\r\n\r\n") { - return Ok(response); - } - } -} - -async fn recv_and_check_response(stream: &mut BufStream) -> Result<(), ProxyError> -where - IO: AsyncRead + AsyncWrite + Unpin, -{ - let response_string = get_response(stream).await?; - - let mut response_headers = [EMPTY_HEADER; MAXIMUM_RESPONSE_HEADERS]; - let mut response = Response::new(&mut response_headers); - let response_bytes = response_string.into_bytes(); - response.parse(&response_bytes)?; - - return match response.code { - Some(code) => { - if code == 200 { - Ok(()) - } else { - Err(ProxyError::HttpCode200(code)) - } - } - None => Err(ProxyError::NoHttpCode), - }; -} diff --git a/libs/hbb_common/src/socket_client.rs b/libs/hbb_common/src/socket_client.rs deleted file mode 100644 index 4cb0bf204b52..000000000000 --- a/libs/hbb_common/src/socket_client.rs +++ /dev/null @@ -1,291 +0,0 @@ -use crate::{ - config::{Config, NetworkType}, - tcp::FramedStream, - udp::FramedSocket, - ResultType, -}; -use anyhow::Context; -use std::net::SocketAddr; -use tokio::net::ToSocketAddrs; -use tokio_socks::{IntoTargetAddr, TargetAddr}; - -#[inline] -pub fn check_port(host: T, port: i32) -> String { - let host = host.to_string(); - if crate::is_ipv6_str(&host) { - if host.starts_with('[') { - return host; - } - return format!("[{host}]:{port}"); - } - if !host.contains(':') { - return format!("{host}:{port}"); - } - host -} - -#[inline] -pub fn increase_port(host: T, offset: i32) -> String { - let host = host.to_string(); - if crate::is_ipv6_str(&host) { - if host.starts_with('[') { - let tmp: Vec<&str> = host.split("]:").collect(); - if tmp.len() == 2 { - let port: i32 = tmp[1].parse().unwrap_or(0); - if port > 0 { - return format!("{}]:{}", tmp[0], port + offset); - } - } - } - } else if host.contains(':') { - let tmp: Vec<&str> = host.split(':').collect(); - if tmp.len() == 2 { - let port: i32 = tmp[1].parse().unwrap_or(0); - if port > 0 { - return format!("{}:{}", tmp[0], port + offset); - } - } - } - host -} - -pub fn test_if_valid_server(host: &str, test_with_proxy: bool) -> String { - let host = check_port(host, 0); - use std::net::ToSocketAddrs; - - if test_with_proxy && NetworkType::ProxySocks == Config::get_network_type() { - test_if_valid_server_for_proxy_(&host) - } else { - match host.to_socket_addrs() { - Err(err) => err.to_string(), - Ok(_) => "".to_owned(), - } - } -} - -#[inline] -pub fn test_if_valid_server_for_proxy_(host: &str) -> String { - // `&host.into_target_addr()` is defined in `tokio-socs`, but is a common pattern for testing, - // it can be used for both `socks` and `http` proxy. - match &host.into_target_addr() { - Err(err) => err.to_string(), - Ok(_) => "".to_owned(), - } -} - -pub trait IsResolvedSocketAddr { - fn resolve(&self) -> Option<&SocketAddr>; -} - -impl IsResolvedSocketAddr for SocketAddr { - fn resolve(&self) -> Option<&SocketAddr> { - Some(self) - } -} - -impl IsResolvedSocketAddr for String { - fn resolve(&self) -> Option<&SocketAddr> { - None - } -} - -impl IsResolvedSocketAddr for &str { - fn resolve(&self) -> Option<&SocketAddr> { - None - } -} - -#[inline] -pub async fn connect_tcp< - 't, - T: IntoTargetAddr<'t> + ToSocketAddrs + IsResolvedSocketAddr + std::fmt::Display, ->( - target: T, - ms_timeout: u64, -) -> ResultType { - connect_tcp_local(target, None, ms_timeout).await -} - -pub async fn connect_tcp_local< - 't, - T: IntoTargetAddr<'t> + ToSocketAddrs + IsResolvedSocketAddr + std::fmt::Display, ->( - target: T, - local: Option, - ms_timeout: u64, -) -> ResultType { - if let Some(conf) = Config::get_socks() { - return FramedStream::connect(target, local, &conf, ms_timeout).await; - } - if let Some(target) = target.resolve() { - if let Some(local) = local { - if local.is_ipv6() && target.is_ipv4() { - let target = query_nip_io(target).await?; - return FramedStream::new(target, Some(local), ms_timeout).await; - } - } - } - FramedStream::new(target, local, ms_timeout).await -} - -#[inline] -pub fn is_ipv4(target: &TargetAddr<'_>) -> bool { - match target { - TargetAddr::Ip(addr) => addr.is_ipv4(), - _ => true, - } -} - -#[inline] -pub async fn query_nip_io(addr: &SocketAddr) -> ResultType { - tokio::net::lookup_host(format!("{}.nip.io:{}", addr.ip(), addr.port())) - .await? - .find(|x| x.is_ipv6()) - .context("Failed to get ipv6 from nip.io") -} - -#[inline] -pub fn ipv4_to_ipv6(addr: String, ipv4: bool) -> String { - if !ipv4 && crate::is_ipv4_str(&addr) { - if let Some(ip) = addr.split(':').next() { - return addr.replace(ip, &format!("{ip}.nip.io")); - } - } - addr -} - -async fn test_target(target: &str) -> ResultType { - if let Ok(Ok(s)) = super::timeout(1000, tokio::net::TcpStream::connect(target)).await { - if let Ok(addr) = s.peer_addr() { - return Ok(addr); - } - } - tokio::net::lookup_host(target) - .await? - .next() - .context(format!("Failed to look up host for {target}")) -} - -#[inline] -pub async fn new_udp_for( - target: &str, - ms_timeout: u64, -) -> ResultType<(FramedSocket, TargetAddr<'static>)> { - let (ipv4, target) = if NetworkType::Direct == Config::get_network_type() { - let addr = test_target(target).await?; - (addr.is_ipv4(), addr.into_target_addr()?) - } else { - (true, target.into_target_addr()?) - }; - Ok(( - new_udp(Config::get_any_listen_addr(ipv4), ms_timeout).await?, - target.to_owned(), - )) -} - -async fn new_udp(local: T, ms_timeout: u64) -> ResultType { - match Config::get_socks() { - None => Ok(FramedSocket::new(local).await?), - Some(conf) => { - let socket = FramedSocket::new_proxy( - conf.proxy.as_str(), - local, - conf.username.as_str(), - conf.password.as_str(), - ms_timeout, - ) - .await?; - Ok(socket) - } - } -} - -pub async fn rebind_udp_for( - target: &str, -) -> ResultType)>> { - if Config::get_network_type() != NetworkType::Direct { - return Ok(None); - } - let addr = test_target(target).await?; - let v4 = addr.is_ipv4(); - Ok(Some(( - FramedSocket::new(Config::get_any_listen_addr(v4)).await?, - addr.into_target_addr()?.to_owned(), - ))) -} - -#[cfg(test)] -mod tests { - use std::net::ToSocketAddrs; - - use super::*; - - #[test] - fn test_nat64() { - test_nat64_async(); - } - - #[tokio::main(flavor = "current_thread")] - async fn test_nat64_async() { - assert_eq!(ipv4_to_ipv6("1.1.1.1".to_owned(), true), "1.1.1.1"); - assert_eq!(ipv4_to_ipv6("1.1.1.1".to_owned(), false), "1.1.1.1.nip.io"); - assert_eq!( - ipv4_to_ipv6("1.1.1.1:8080".to_owned(), false), - "1.1.1.1.nip.io:8080" - ); - assert_eq!( - ipv4_to_ipv6("rustdesk.com".to_owned(), false), - "rustdesk.com" - ); - if ("rustdesk.com:80") - .to_socket_addrs() - .unwrap() - .next() - .unwrap() - .is_ipv6() - { - assert!(query_nip_io(&"1.1.1.1:80".parse().unwrap()) - .await - .unwrap() - .is_ipv6()); - return; - } - assert!(query_nip_io(&"1.1.1.1:80".parse().unwrap()).await.is_err()); - } - - #[test] - fn test_test_if_valid_server() { - assert!(!test_if_valid_server("a", false).is_empty()); - // on Linux, "1" is resolved to "0.0.0.1" - assert!(test_if_valid_server("1.1.1.1", false).is_empty()); - assert!(test_if_valid_server("1.1.1.1:1", false).is_empty()); - assert!(test_if_valid_server("microsoft.com", false).is_empty()); - assert!(test_if_valid_server("microsoft.com:1", false).is_empty()); - - // with proxy - // `:0` indicates `let host = check_port(host, 0);` is called. - assert!(test_if_valid_server_for_proxy_("a:0").is_empty()); - assert!(test_if_valid_server_for_proxy_("1.1.1.1:0").is_empty()); - assert!(test_if_valid_server_for_proxy_("1.1.1.1:1").is_empty()); - assert!(test_if_valid_server_for_proxy_("abc.com:0").is_empty()); - assert!(test_if_valid_server_for_proxy_("abcd.com:1").is_empty()); - } - - #[test] - fn test_check_port() { - assert_eq!(check_port("[1:2]:12", 32), "[1:2]:12"); - assert_eq!(check_port("1:2", 32), "[1:2]:32"); - assert_eq!(check_port("z1:2", 32), "z1:2"); - assert_eq!(check_port("1.1.1.1", 32), "1.1.1.1:32"); - assert_eq!(check_port("1.1.1.1:32", 32), "1.1.1.1:32"); - assert_eq!(check_port("test.com:32", 0), "test.com:32"); - assert_eq!(increase_port("[1:2]:12", 1), "[1:2]:13"); - assert_eq!(increase_port("1.2.2.4:12", 1), "1.2.2.4:13"); - assert_eq!(increase_port("1.2.2.4", 1), "1.2.2.4"); - assert_eq!(increase_port("test.com", 1), "test.com"); - assert_eq!(increase_port("test.com:13", 4), "test.com:17"); - assert_eq!(increase_port("1:13", 4), "1:13"); - assert_eq!(increase_port("22:1:13", 4), "22:1:13"); - assert_eq!(increase_port("z1:2", 1), "z1:3"); - } -} diff --git a/libs/hbb_common/src/tcp.rs b/libs/hbb_common/src/tcp.rs deleted file mode 100644 index 17f360ff90cb..000000000000 --- a/libs/hbb_common/src/tcp.rs +++ /dev/null @@ -1,341 +0,0 @@ -use crate::{bail, bytes_codec::BytesCodec, ResultType, config::Socks5Server, proxy::Proxy}; -use anyhow::Context as AnyhowCtx; -use bytes::{BufMut, Bytes, BytesMut}; -use futures::{SinkExt, StreamExt}; -use protobuf::Message; -use sodiumoxide::crypto::{ - box_, - secretbox::{self, Key, Nonce}, -}; -use std::{ - io::{self, Error, ErrorKind}, - net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}, - ops::{Deref, DerefMut}, - pin::Pin, - task::{Context, Poll}, -}; -use tokio::{ - io::{AsyncRead, AsyncWrite, ReadBuf}, - net::{lookup_host, TcpListener, TcpSocket, ToSocketAddrs}, -}; -use tokio_socks::IntoTargetAddr; -use tokio_util::codec::Framed; - -pub trait TcpStreamTrait: AsyncRead + AsyncWrite + Unpin {} -pub struct DynTcpStream(pub(crate) Box); - -#[derive(Clone)] -pub struct Encrypt(Key, u64, u64); - -pub struct FramedStream( - pub(crate) Framed, - pub(crate) SocketAddr, - pub(crate) Option, - pub(crate) u64, -); - -impl Deref for FramedStream { - type Target = Framed; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl DerefMut for FramedStream { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } -} - -impl Deref for DynTcpStream { - type Target = Box; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl DerefMut for DynTcpStream { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } -} - -pub(crate) fn new_socket(addr: std::net::SocketAddr, reuse: bool) -> Result { - let socket = match addr { - std::net::SocketAddr::V4(..) => TcpSocket::new_v4()?, - std::net::SocketAddr::V6(..) => TcpSocket::new_v6()?, - }; - if reuse { - // windows has no reuse_port, but it's reuse_address - // almost equals to unix's reuse_port + reuse_address, - // though may introduce nondeterministic behavior - #[cfg(unix)] - socket.set_reuseport(true).ok(); - socket.set_reuseaddr(true).ok(); - } - socket.bind(addr)?; - Ok(socket) -} - -impl FramedStream { - pub async fn new( - remote_addr: T, - local_addr: Option, - ms_timeout: u64, - ) -> ResultType { - for remote_addr in lookup_host(&remote_addr).await? { - let local = if let Some(addr) = local_addr { - addr - } else { - crate::config::Config::get_any_listen_addr(remote_addr.is_ipv4()) - }; - if let Ok(socket) = new_socket(local, true) { - if let Ok(Ok(stream)) = - super::timeout(ms_timeout, socket.connect(remote_addr)).await - { - stream.set_nodelay(true).ok(); - let addr = stream.local_addr()?; - return Ok(Self( - Framed::new(DynTcpStream(Box::new(stream)), BytesCodec::new()), - addr, - None, - 0, - )); - } - } - } - bail!(format!("Failed to connect to {remote_addr}")); - } - - pub async fn connect<'t, T>( - target: T, - local_addr: Option, - proxy_conf: &Socks5Server, - ms_timeout: u64, - ) -> ResultType - where - T: IntoTargetAddr<'t>, - { - let proxy = Proxy::from_conf(proxy_conf, Some(ms_timeout))?; - proxy.connect::(target, local_addr).await - } - - pub fn local_addr(&self) -> SocketAddr { - self.1 - } - - pub fn set_send_timeout(&mut self, ms: u64) { - self.3 = ms; - } - - pub fn from(stream: impl TcpStreamTrait + Send + Sync + 'static, addr: SocketAddr) -> Self { - Self( - Framed::new(DynTcpStream(Box::new(stream)), BytesCodec::new()), - addr, - None, - 0, - ) - } - - pub fn set_raw(&mut self) { - self.0.codec_mut().set_raw(); - self.2 = None; - } - - pub fn is_secured(&self) -> bool { - self.2.is_some() - } - - #[inline] - pub async fn send(&mut self, msg: &impl Message) -> ResultType<()> { - self.send_raw(msg.write_to_bytes()?).await - } - - #[inline] - pub async fn send_raw(&mut self, msg: Vec) -> ResultType<()> { - let mut msg = msg; - if let Some(key) = self.2.as_mut() { - msg = key.enc(&msg); - } - self.send_bytes(bytes::Bytes::from(msg)).await?; - Ok(()) - } - - #[inline] - pub async fn send_bytes(&mut self, bytes: Bytes) -> ResultType<()> { - if self.3 > 0 { - super::timeout(self.3, self.0.send(bytes)).await??; - } else { - self.0.send(bytes).await?; - } - Ok(()) - } - - #[inline] - pub async fn next(&mut self) -> Option> { - let mut res = self.0.next().await; - if let Some(Ok(bytes)) = res.as_mut() { - if let Some(key) = self.2.as_mut() { - if let Err(err) = key.dec(bytes) { - return Some(Err(err)); - } - } - } - res - } - - #[inline] - pub async fn next_timeout(&mut self, ms: u64) -> Option> { - if let Ok(res) = super::timeout(ms, self.next()).await { - res - } else { - None - } - } - - pub fn set_key(&mut self, key: Key) { - self.2 = Some(Encrypt::new(key)); - } - - fn get_nonce(seqnum: u64) -> Nonce { - let mut nonce = Nonce([0u8; secretbox::NONCEBYTES]); - nonce.0[..std::mem::size_of_val(&seqnum)].copy_from_slice(&seqnum.to_le_bytes()); - nonce - } -} - -const DEFAULT_BACKLOG: u32 = 128; - -pub async fn new_listener(addr: T, reuse: bool) -> ResultType { - if !reuse { - Ok(TcpListener::bind(addr).await?) - } else { - let addr = lookup_host(&addr) - .await? - .next() - .context("could not resolve to any address")?; - new_socket(addr, true)? - .listen(DEFAULT_BACKLOG) - .map_err(anyhow::Error::msg) - } -} - -pub async fn listen_any(port: u16) -> ResultType { - if let Ok(mut socket) = TcpSocket::new_v6() { - #[cfg(unix)] - { - socket.set_reuseport(true).ok(); - socket.set_reuseaddr(true).ok(); - use std::os::unix::io::{FromRawFd, IntoRawFd}; - let raw_fd = socket.into_raw_fd(); - let sock2 = unsafe { socket2::Socket::from_raw_fd(raw_fd) }; - sock2.set_only_v6(false).ok(); - socket = unsafe { TcpSocket::from_raw_fd(sock2.into_raw_fd()) }; - } - #[cfg(windows)] - { - use std::os::windows::prelude::{FromRawSocket, IntoRawSocket}; - let raw_socket = socket.into_raw_socket(); - let sock2 = unsafe { socket2::Socket::from_raw_socket(raw_socket) }; - sock2.set_only_v6(false).ok(); - socket = unsafe { TcpSocket::from_raw_socket(sock2.into_raw_socket()) }; - } - if socket - .bind(SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), port)) - .is_ok() - { - if let Ok(l) = socket.listen(DEFAULT_BACKLOG) { - return Ok(l); - } - } - } - Ok(new_socket( - SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), port), - true, - )? - .listen(DEFAULT_BACKLOG)?) -} - -impl Unpin for DynTcpStream {} - -impl AsyncRead for DynTcpStream { - fn poll_read( - mut self: Pin<&mut Self>, - cx: &mut Context<'_>, - buf: &mut ReadBuf<'_>, - ) -> Poll> { - AsyncRead::poll_read(Pin::new(&mut self.0), cx, buf) - } -} - -impl AsyncWrite for DynTcpStream { - fn poll_write( - mut self: Pin<&mut Self>, - cx: &mut Context<'_>, - buf: &[u8], - ) -> Poll> { - AsyncWrite::poll_write(Pin::new(&mut self.0), cx, buf) - } - - fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - AsyncWrite::poll_flush(Pin::new(&mut self.0), cx) - } - - fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - AsyncWrite::poll_shutdown(Pin::new(&mut self.0), cx) - } -} - -impl TcpStreamTrait for R {} - -impl Encrypt { - pub fn new(key: Key) -> Self { - Self(key, 0, 0) - } - - pub fn dec(&mut self, bytes: &mut BytesMut) -> Result<(), Error> { - if bytes.len() <= 1 { - return Ok(()); - } - self.2 += 1; - let nonce = FramedStream::get_nonce(self.2); - match secretbox::open(bytes, &nonce, &self.0) { - Ok(res) => { - bytes.clear(); - bytes.put_slice(&res); - Ok(()) - } - Err(()) => Err(Error::new(ErrorKind::Other, "decryption error")), - } - } - - pub fn enc(&mut self, data: &[u8]) -> Vec { - self.1 += 1; - let nonce = FramedStream::get_nonce(self.1); - secretbox::seal(&data, &nonce, &self.0) - } - - pub fn decode( - symmetric_data: &[u8], - their_pk_b: &[u8], - our_sk_b: &box_::SecretKey, - ) -> ResultType { - if their_pk_b.len() != box_::PUBLICKEYBYTES { - anyhow::bail!("Handshake failed: pk length {}", their_pk_b.len()); - } - let nonce = box_::Nonce([0u8; box_::NONCEBYTES]); - let mut pk_ = [0u8; box_::PUBLICKEYBYTES]; - pk_[..].copy_from_slice(their_pk_b); - let their_pk_b = box_::PublicKey(pk_); - let symmetric_key = box_::open(symmetric_data, &nonce, &their_pk_b, &our_sk_b) - .map_err(|_| anyhow::anyhow!("Handshake failed: box decryption failure"))?; - if symmetric_key.len() != secretbox::KEYBYTES { - anyhow::bail!("Handshake failed: invalid secret key length from peer"); - } - let mut key = [0u8; secretbox::KEYBYTES]; - key[..].copy_from_slice(&symmetric_key); - Ok(Key(key)) - } -} diff --git a/libs/hbb_common/src/udp.rs b/libs/hbb_common/src/udp.rs deleted file mode 100644 index 68abd42df9a3..000000000000 --- a/libs/hbb_common/src/udp.rs +++ /dev/null @@ -1,170 +0,0 @@ -use crate::ResultType; -use anyhow::{anyhow, Context}; -use bytes::{Bytes, BytesMut}; -use futures::{SinkExt, StreamExt}; -use protobuf::Message; -use socket2::{Domain, Socket, Type}; -use std::net::SocketAddr; -use tokio::net::{lookup_host, ToSocketAddrs, UdpSocket}; -use tokio_socks::{udp::Socks5UdpFramed, IntoTargetAddr, TargetAddr, ToProxyAddrs}; -use tokio_util::{codec::BytesCodec, udp::UdpFramed}; - -pub enum FramedSocket { - Direct(UdpFramed), - ProxySocks(Socks5UdpFramed), -} - -fn new_socket(addr: SocketAddr, reuse: bool, buf_size: usize) -> Result { - let socket = match addr { - SocketAddr::V4(..) => Socket::new(Domain::ipv4(), Type::dgram(), None), - SocketAddr::V6(..) => Socket::new(Domain::ipv6(), Type::dgram(), None), - }?; - if reuse { - // windows has no reuse_port, but it's reuse_address - // almost equals to unix's reuse_port + reuse_address, - // though may introduce nondeterministic behavior - #[cfg(unix)] - socket.set_reuse_port(true).ok(); - socket.set_reuse_address(true).ok(); - } - // only nonblocking work with tokio, https://stackoverflow.com/questions/64649405/receiver-on-tokiompscchannel-only-receives-messages-when-buffer-is-full - socket.set_nonblocking(true)?; - if buf_size > 0 { - socket.set_recv_buffer_size(buf_size).ok(); - } - log::debug!( - "Receive buf size of udp {}: {:?}", - addr, - socket.recv_buffer_size() - ); - if addr.is_ipv6() && addr.ip().is_unspecified() && addr.port() > 0 { - socket.set_only_v6(false).ok(); - } - socket.bind(&addr.into())?; - Ok(socket) -} - -impl FramedSocket { - pub async fn new(addr: T) -> ResultType { - Self::new_reuse(addr, false, 0).await - } - - pub async fn new_reuse( - addr: T, - reuse: bool, - buf_size: usize, - ) -> ResultType { - let addr = lookup_host(&addr) - .await? - .next() - .context("could not resolve to any address")?; - Ok(Self::Direct(UdpFramed::new( - UdpSocket::from_std(new_socket(addr, reuse, buf_size)?.into_udp_socket())?, - BytesCodec::new(), - ))) - } - - pub async fn new_proxy<'a, 't, P: ToProxyAddrs, T: ToSocketAddrs>( - proxy: P, - local: T, - username: &'a str, - password: &'a str, - ms_timeout: u64, - ) -> ResultType { - let framed = if username.trim().is_empty() { - super::timeout(ms_timeout, Socks5UdpFramed::connect(proxy, Some(local))).await?? - } else { - super::timeout( - ms_timeout, - Socks5UdpFramed::connect_with_password(proxy, Some(local), username, password), - ) - .await?? - }; - log::trace!( - "Socks5 udp connected, local addr: {:?}, target addr: {}", - framed.local_addr(), - framed.socks_addr() - ); - Ok(Self::ProxySocks(framed)) - } - - #[inline] - pub async fn send( - &mut self, - msg: &impl Message, - addr: impl IntoTargetAddr<'_>, - ) -> ResultType<()> { - let addr = addr.into_target_addr()?.to_owned(); - let send_data = Bytes::from(msg.write_to_bytes()?); - match self { - Self::Direct(f) => { - if let TargetAddr::Ip(addr) = addr { - f.send((send_data, addr)).await? - } - } - Self::ProxySocks(f) => f.send((send_data, addr)).await?, - }; - Ok(()) - } - - // https://stackoverflow.com/a/68733302/1926020 - #[inline] - pub async fn send_raw( - &mut self, - msg: &'static [u8], - addr: impl IntoTargetAddr<'static>, - ) -> ResultType<()> { - let addr = addr.into_target_addr()?.to_owned(); - - match self { - Self::Direct(f) => { - if let TargetAddr::Ip(addr) = addr { - f.send((Bytes::from(msg), addr)).await? - } - } - Self::ProxySocks(f) => f.send((Bytes::from(msg), addr)).await?, - }; - Ok(()) - } - - #[inline] - pub async fn next(&mut self) -> Option)>> { - match self { - Self::Direct(f) => match f.next().await { - Some(Ok((data, addr))) => { - Some(Ok((data, addr.into_target_addr().ok()?.to_owned()))) - } - Some(Err(e)) => Some(Err(anyhow!(e))), - None => None, - }, - Self::ProxySocks(f) => match f.next().await { - Some(Ok((data, _))) => Some(Ok((data.data, data.dst_addr))), - Some(Err(e)) => Some(Err(anyhow!(e))), - None => None, - }, - } - } - - #[inline] - pub async fn next_timeout( - &mut self, - ms: u64, - ) -> Option)>> { - if let Ok(res) = - tokio::time::timeout(std::time::Duration::from_millis(ms), self.next()).await - { - res - } else { - None - } - } - - pub fn local_addr(&self) -> Option { - if let FramedSocket::Direct(x) = self { - if let Ok(v) = x.get_ref().local_addr() { - return Some(v); - } - } - None - } -} diff --git a/src/client.rs b/src/client.rs index 05161438325f..6833ad34f700 100644 --- a/src/client.rs +++ b/src/client.rs @@ -12,7 +12,6 @@ use magnum_opus::{Channels::*, Decoder as AudioDecoder}; #[cfg(not(target_os = "linux"))] use ringbuf::{ring_buffer::RbBase, Rb}; use serde::{Deserialize, Serialize}; -use sha2::{Digest, Sha256}; use std::{ collections::HashMap, ffi::c_void, @@ -31,6 +30,7 @@ pub use file_trait::FileManager; #[cfg(not(feature = "flutter"))] #[cfg(not(any(target_os = "android", target_os = "ios")))] use hbb_common::tokio::sync::mpsc::UnboundedSender; +use hbb_common::tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver}; use hbb_common::{ allow_err, anyhow::{anyhow, Context}, @@ -44,6 +44,7 @@ use hbb_common::{ protobuf::{Message as _, MessageField}, rand, rendezvous_proto::*, + sha2::{Digest, Sha256}, socket_client::{connect_tcp, connect_tcp_local, ipv4_to_ipv6}, sodiumoxide::{base64, crypto::sign}, tcp::FramedStream, @@ -54,10 +55,6 @@ use hbb_common::{ }, AddrMangle, ResultType, Stream, }; -use hbb_common::{ - config::keys::OPTION_ALLOW_AUTO_RECORD_OUTGOING, - tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver}, -}; pub use helper::*; use scrap::{ codec::Decoder, diff --git a/src/common.rs b/src/common.rs index 294ab97cc4f7..14db9b5d8955 100644 --- a/src/common.rs +++ b/src/common.rs @@ -816,16 +816,17 @@ pub fn check_software_update() { #[tokio::main(flavor = "current_thread")] async fn check_software_update_() -> hbb_common::ResultType<()> { - let url = "https://github.com/rustdesk/rustdesk/releases/latest"; - let latest_release_response = create_http_client_async().get(url).send().await?; - let latest_release_version = latest_release_response - .url() - .path() - .rsplit('/') - .next() - .unwrap_or_default(); - - let response_url = latest_release_response.url().to_string(); + let (request, url) = + hbb_common::version_check_request(hbb_common::VER_TYPE_RUSTDESK_CLIENT.to_string()); + let latest_release_response = create_http_client_async() + .post(url) + .json(&request) + .send() + .await?; + let bytes = latest_release_response.bytes().await?; + let resp: hbb_common::VersionCheckResponse = serde_json::from_slice(&bytes)?; + let response_url = resp.url; + let latest_release_version = response_url.rsplit('/').next().unwrap_or_default(); if get_version_number(&latest_release_version) > get_version_number(crate::VERSION) { #[cfg(feature = "flutter")] @@ -1541,7 +1542,7 @@ pub fn is_empty_uni_link(arg: &str) -> bool { } pub fn get_hwid() -> Bytes { - use sha2::{Digest, Sha256}; + use hbb_common::sha2::{Digest, Sha256}; let uuid = hbb_common::get_uuid(); let mut hasher = Sha256::new(); diff --git a/src/server/connection.rs b/src/server/connection.rs index 153740c28a3d..93e45336342f 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -33,6 +33,7 @@ use hbb_common::{ get_time, get_version_number, message_proto::{option_message::BoolOption, permission_info::Permission}, password_security::{self as password, ApproveMode}, + sha2::{Digest, Sha256}, sleep, timeout, tokio::{ net::TcpStream, @@ -45,7 +46,6 @@ use hbb_common::{ use scrap::android::{call_main_service_key_event, call_main_service_pointer_input}; use serde_derive::Serialize; use serde_json::{json, value::Value}; -use sha2::{Digest, Sha256}; #[cfg(not(any(target_os = "android", target_os = "ios")))] use std::sync::atomic::Ordering; use std::{ From 5fa8c25e65cba1c437b4c252451394b41e66edda Mon Sep 17 00:00:00 2001 From: 21pages Date: Mon, 20 Jan 2025 17:59:36 +0800 Subject: [PATCH 524/541] opt qos (#10459) * Adjust bitrate and fps based on TestDelay messages. * Bitrate is adjusted every 3 seconds, fps is adjusted every second and when receiving test lag. * Latency optimized at high resolutions. However, when the network is poor, the delay when just connecting or sliding static pages is still obvious. Signed-off-by: 21pages --- libs/scrap/examples/benchmark.rs | 27 +- libs/scrap/examples/record-screen.rs | 20 +- libs/scrap/src/common/aom.rs | 74 +-- libs/scrap/src/common/codec.rs | 68 ++- libs/scrap/src/common/hwcodec.rs | 68 +-- libs/scrap/src/common/vpxcodec.rs | 73 +-- libs/scrap/src/common/vram.rs | 61 +-- src/server/connection.rs | 34 +- src/server/video_qos.rs | 739 +++++++++++++++++---------- src/server/video_service.rs | 87 ++-- 10 files changed, 719 insertions(+), 532 deletions(-) diff --git a/libs/scrap/examples/benchmark.rs b/libs/scrap/examples/benchmark.rs index 803a4343ce26..a867a2d3fcc6 100644 --- a/libs/scrap/examples/benchmark.rs +++ b/libs/scrap/examples/benchmark.rs @@ -5,7 +5,7 @@ use hbb_common::{ }; use scrap::{ aom::{AomDecoder, AomEncoder, AomEncoderConfig}, - codec::{EncoderApi, EncoderCfg, Quality as Q}, + codec::{EncoderApi, EncoderCfg}, Capturer, Display, TraitCapturer, VpxDecoder, VpxDecoderConfig, VpxEncoder, VpxEncoderConfig, VpxVideoCodecId::{self, *}, STRIDE_ALIGN, @@ -27,25 +27,17 @@ Usage: Options: -h --help Show this screen. --count=COUNT Capture frame count [default: 100]. - --quality=QUALITY Video quality [default: Balanced]. - Valid values: Best, Balanced, Low. + --quality=QUALITY Video quality [default: 1.0]. --i444 I444. "; #[derive(Debug, serde::Deserialize, Clone, Copy)] struct Args { flag_count: usize, - flag_quality: Quality, + flag_quality: f32, flag_i444: bool, } -#[derive(Debug, serde::Deserialize, Clone, Copy)] -enum Quality { - Best, - Balanced, - Low, -} - fn main() { init_from_env(Env::default().filter_or(DEFAULT_FILTER_ENV, "info")); let args: Args = Docopt::new(USAGE) @@ -70,11 +62,6 @@ fn main() { "benchmark {}x{} quality:{:?}, i444:{:?}", width, height, quality, args.flag_i444 ); - let quality = match quality { - Quality::Best => Q::Best, - Quality::Balanced => Q::Balanced, - Quality::Low => Q::Low, - }; [VP8, VP9].map(|codec| { test_vpx( &mut c, @@ -98,7 +85,7 @@ fn test_vpx( codec_id: VpxVideoCodecId, width: usize, height: usize, - quality: Q, + quality: f32, yuv_count: usize, i444: bool, ) { @@ -177,7 +164,7 @@ fn test_av1( c: &mut Capturer, width: usize, height: usize, - quality: Q, + quality: f32, yuv_count: usize, i444: bool, ) { @@ -247,7 +234,7 @@ mod hw { use super::*; - pub fn test(c: &mut Capturer, width: usize, height: usize, quality: Q, yuv_count: usize) { + pub fn test(c: &mut Capturer, width: usize, height: usize, quality: f32, yuv_count: usize) { let mut h264s = Vec::new(); let mut h265s = Vec::new(); if let Some(info) = HwRamEncoder::try_get(CodecFormat::H264) { @@ -263,7 +250,7 @@ mod hw { fn test_encoder( width: usize, height: usize, - quality: Q, + quality: f32, info: CodecInfo, c: &mut Capturer, yuv_count: usize, diff --git a/libs/scrap/examples/record-screen.rs b/libs/scrap/examples/record-screen.rs index 6d68a7352f96..ca620608adbe 100644 --- a/libs/scrap/examples/record-screen.rs +++ b/libs/scrap/examples/record-screen.rs @@ -13,7 +13,7 @@ use std::time::{Duration, Instant}; use std::{io, thread}; use docopt::Docopt; -use scrap::codec::{EncoderApi, EncoderCfg, Quality as Q}; +use scrap::codec::{EncoderApi, EncoderCfg}; use webm::mux; use webm::mux::Track; @@ -31,8 +31,7 @@ Options: -h --help Show this screen. --time= Recording duration in seconds. --fps= Frames per second [default: 30]. - --quality= Video quality [default: Balanced]. - Valid values: Best, Balanced, Low. + --quality= Video quality [default: 1.0]. --ba= Audio bitrate in kilobits per second [default: 96]. --codec CODEC Configure the codec used. [default: vp9] Valid values: vp8, vp9. @@ -44,14 +43,7 @@ struct Args { flag_codec: Codec, flag_time: Option, flag_fps: u64, - flag_quality: Quality, -} - -#[derive(Debug, serde::Deserialize)] -enum Quality { - Best, - Balanced, - Low, + flag_quality: f32, } #[derive(Debug, serde::Deserialize)] @@ -105,11 +97,7 @@ fn main() -> io::Result<()> { let mut vt = webm.add_video_track(width, height, None, mux_codec); // Setup the encoder. - let quality = match args.flag_quality { - Quality::Best => Q::Best, - Quality::Balanced => Q::Balanced, - Quality::Low => Q::Low, - }; + let quality = args.flag_quality; let mut vpx = vpx_encode::VpxEncoder::new( EncoderCfg::VPX(vpx_encode::VpxEncoderConfig { width, diff --git a/libs/scrap/src/common/aom.rs b/libs/scrap/src/common/aom.rs index d2bb2feb77f2..4bf17a2fee3a 100644 --- a/libs/scrap/src/common/aom.rs +++ b/libs/scrap/src/common/aom.rs @@ -6,7 +6,7 @@ include!(concat!(env!("OUT_DIR"), "/aom_ffi.rs")); -use crate::codec::{base_bitrate, codec_thread_num, Quality}; +use crate::codec::{base_bitrate, codec_thread_num}; use crate::{codec::EncoderApi, EncodeFrame, STRIDE_ALIGN}; use crate::{common::GoogleImage, generate_call_macro, generate_call_ptr_macro, Error, Result}; use crate::{EncodeInput, EncodeYuvFormat, Pixfmt}; @@ -45,7 +45,7 @@ impl Default for aom_image_t { pub struct AomEncoderConfig { pub width: u32, pub height: u32, - pub quality: Quality, + pub quality: f32, pub keyframe_interval: Option, } @@ -62,15 +62,9 @@ mod webrtc { use super::*; const kUsageProfile: u32 = AOM_USAGE_REALTIME; - const kMinQindex: u32 = 145; // Min qindex threshold for QP scaling. - const kMaxQindex: u32 = 205; // Max qindex threshold for QP scaling. const kBitDepth: u32 = 8; const kLagInFrames: u32 = 0; // No look ahead. pub(super) const kTimeBaseDen: i64 = 1000; - const kMinimumFrameRate: f64 = 1.0; - - pub const DEFAULT_Q_MAX: u32 = 56; // no more than 63 - pub const DEFAULT_Q_MIN: u32 = 12; // no more than 63, litter than q_max // Only positive speeds, range for real-time coding currently is: 6 - 8. // Lower means slower/better quality, higher means fastest/lower quality. @@ -116,21 +110,10 @@ mod webrtc { } else { c.kf_mode = aom_kf_mode::AOM_KF_DISABLED; } - let (q_min, q_max, b) = AomEncoder::convert_quality(cfg.quality); - if q_min > 0 && q_min < q_max && q_max < 64 { - c.rc_min_quantizer = q_min; - c.rc_max_quantizer = q_max; - } else { - c.rc_min_quantizer = DEFAULT_Q_MIN; - c.rc_max_quantizer = DEFAULT_Q_MAX; - } - let base_bitrate = base_bitrate(cfg.width as _, cfg.height as _); - let bitrate = base_bitrate * b / 100; - if bitrate > 0 { - c.rc_target_bitrate = bitrate; - } else { - c.rc_target_bitrate = base_bitrate; - } + let (q_min, q_max) = AomEncoder::calc_q_values(cfg.quality); + c.rc_min_quantizer = q_min; + c.rc_max_quantizer = q_max; + c.rc_target_bitrate = AomEncoder::bitrate(cfg.width as _, cfg.height as _, cfg.quality); c.rc_undershoot_pct = 50; c.rc_overshoot_pct = 50; c.rc_buf_initial_sz = 600; @@ -273,17 +256,12 @@ impl EncoderApi for AomEncoder { false } - fn set_quality(&mut self, quality: Quality) -> ResultType<()> { + fn set_quality(&mut self, ratio: f32) -> ResultType<()> { let mut c = unsafe { *self.ctx.config.enc.to_owned() }; - let (q_min, q_max, b) = Self::convert_quality(quality); - if q_min > 0 && q_min < q_max && q_max < 64 { - c.rc_min_quantizer = q_min; - c.rc_max_quantizer = q_max; - } - let bitrate = base_bitrate(self.width as _, self.height as _) * b / 100; - if bitrate > 0 { - c.rc_target_bitrate = bitrate; - } + let (q_min, q_max) = Self::calc_q_values(ratio); + c.rc_min_quantizer = q_min; + c.rc_max_quantizer = q_max; + c.rc_target_bitrate = Self::bitrate(self.width as _, self.height as _, ratio); call_aom!(aom_codec_enc_config_set(&mut self.ctx, &c)); Ok(()) } @@ -293,10 +271,6 @@ impl EncoderApi for AomEncoder { c.rc_target_bitrate } - fn support_abr(&self) -> bool { - true - } - fn support_changing_quality(&self) -> bool { true } @@ -370,31 +344,27 @@ impl AomEncoder { } } - pub fn convert_quality(quality: Quality) -> (u32, u32, u32) { - // we can use lower bitrate for av1 - match quality { - Quality::Best => (12, 25, 100), - Quality::Balanced => (12, 35, 100 * 2 / 3), - Quality::Low => (18, 45, 50), - Quality::Custom(b) => { - let (q_min, q_max) = Self::calc_q_values(b); - (q_min, q_max, b) - } - } + fn bitrate(width: u32, height: u32, ratio: f32) -> u32 { + let bitrate = base_bitrate(width, height) as f32; + (bitrate * ratio) as u32 } #[inline] - fn calc_q_values(b: u32) -> (u32, u32) { + fn calc_q_values(ratio: f32) -> (u32, u32) { + let b = (ratio * 100.0) as u32; let b = std::cmp::min(b, 200); - let q_min1: i32 = 24; + let q_min1 = 24; let q_min2 = 5; let q_max1 = 45; let q_max2 = 25; let t = b as f32 / 200.0; - let q_min: u32 = ((1.0 - t) * q_min1 as f32 + t * q_min2 as f32).round() as u32; - let q_max = ((1.0 - t) * q_max1 as f32 + t * q_max2 as f32).round() as u32; + let mut q_min: u32 = ((1.0 - t) * q_min1 as f32 + t * q_min2 as f32).round() as u32; + let mut q_max = ((1.0 - t) * q_max1 as f32 + t * q_max2 as f32).round() as u32; + + q_min = q_min.clamp(q_min2, q_min1); + q_max = q_max.clamp(q_max2, q_max1); (q_min, q_max) } diff --git a/libs/scrap/src/common/codec.rs b/libs/scrap/src/common/codec.rs index 648de19b069d..662ac02a5423 100644 --- a/libs/scrap/src/common/codec.rs +++ b/libs/scrap/src/common/codec.rs @@ -62,12 +62,10 @@ pub trait EncoderApi { #[cfg(feature = "vram")] fn input_texture(&self) -> bool; - fn set_quality(&mut self, quality: Quality) -> ResultType<()>; + fn set_quality(&mut self, ratio: f32) -> ResultType<()>; fn bitrate(&self) -> u32; - fn support_abr(&self) -> bool; - fn support_changing_quality(&self) -> bool; fn latency_free(&self) -> bool; @@ -882,12 +880,16 @@ pub fn enable_directx_capture() -> bool { ) } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub const BR_BEST: f32 = 1.5; +pub const BR_BALANCED: f32 = 0.67; +pub const BR_SPEED: f32 = 0.5; + +#[derive(Debug, Clone, Copy, PartialEq)] pub enum Quality { Best, Balanced, Low, - Custom(u32), + Custom(f32), } impl Default for Quality { @@ -903,22 +905,59 @@ impl Quality { _ => false, } } + + pub fn ratio(&self) -> f32 { + match self { + Quality::Best => BR_BEST, + Quality::Balanced => BR_BALANCED, + Quality::Low => BR_SPEED, + Quality::Custom(v) => *v, + } + } } pub fn base_bitrate(width: u32, height: u32) -> u32 { - #[allow(unused_mut)] - let mut base_bitrate = ((width * height) / 1000) as u32; // same as 1.1.9 - if base_bitrate == 0 { - base_bitrate = 1920 * 1080 / 1000; - } + const RESOLUTION_PRESETS: &[(u32, u32, u32)] = &[ + (640, 480, 400), // VGA, 307k pixels + (800, 600, 500), // SVGA, 480k pixels + (1024, 768, 800), // XGA, 786k pixels + (1280, 720, 1000), // 720p, 921k pixels + (1366, 768, 1100), // HD, 1049k pixels + (1440, 900, 1300), // WXGA+, 1296k pixels + (1600, 900, 1500), // HD+, 1440k pixels + (1920, 1080, 2073), // 1080p, 2073k pixels + (2048, 1080, 2200), // 2K DCI, 2211k pixels + (2560, 1440, 3000), // 2K QHD, 3686k pixels + (3440, 1440, 4000), // UWQHD, 4953k pixels + (3840, 2160, 5000), // 4K UHD, 8294k pixels + (7680, 4320, 12000), // 8K UHD, 33177k pixels + ]; + let pixels = width * height; + + let (preset_pixels, preset_bitrate) = RESOLUTION_PRESETS + .iter() + .map(|(w, h, bitrate)| (w * h, bitrate)) + .min_by_key(|(preset_pixels, _)| { + if *preset_pixels >= pixels { + preset_pixels - pixels + } else { + pixels - preset_pixels + } + }) + .unwrap_or(((1920 * 1080) as u32, &2073)); // default 1080p + + let bitrate = (*preset_bitrate as f32 * (pixels as f32 / preset_pixels as f32)).round() as u32; + #[cfg(target_os = "android")] { - // fix when android screen shrinks let fix = crate::Display::fix_quality() as u32; log::debug!("Android screen, fix quality:{}", fix); - base_bitrate = base_bitrate * fix; + bitrate * fix + } + #[cfg(not(target_os = "android"))] + { + bitrate } - base_bitrate } pub fn codec_thread_num(limit: usize) -> usize { @@ -1001,8 +1040,7 @@ pub fn test_av1() { static ONCE: Once = Once::new(); ONCE.call_once(|| { let f = || { - let (width, height, quality, keyframe_interval, i444) = - (1920, 1080, Quality::Balanced, None, false); + let (width, height, quality, keyframe_interval, i444) = (1920, 1080, 1.0, None, false); let frame_count = 10; let block_size = 300; let move_step = 50; diff --git a/libs/scrap/src/common/hwcodec.rs b/libs/scrap/src/common/hwcodec.rs index e4e301066311..7ee9b3d61d57 100644 --- a/libs/scrap/src/common/hwcodec.rs +++ b/libs/scrap/src/common/hwcodec.rs @@ -1,7 +1,5 @@ use crate::{ - codec::{ - base_bitrate, codec_thread_num, enable_hwcodec_option, EncoderApi, EncoderCfg, Quality as Q, - }, + codec::{base_bitrate, codec_thread_num, enable_hwcodec_option, EncoderApi, EncoderCfg}, convert::*, CodecFormat, EncodeInput, ImageFormat, ImageRgb, Pixfmt, HW_STRIDE_ALIGN, }; @@ -47,7 +45,7 @@ pub struct HwRamEncoderConfig { pub mc_name: Option, pub width: usize, pub height: usize, - pub quality: Q, + pub quality: f32, pub keyframe_interval: Option, } @@ -67,12 +65,8 @@ impl EncoderApi for HwRamEncoder { match cfg { EncoderCfg::HWRAM(config) => { let rc = Self::rate_control(&config); - let b = Self::convert_quality(&config.name, config.quality); - let base_bitrate = base_bitrate(config.width as _, config.height as _); - let mut bitrate = base_bitrate * b / 100; - if bitrate <= 0 { - bitrate = base_bitrate; - } + let mut bitrate = + Self::bitrate(&config.name, config.width, config.height, config.quality); bitrate = Self::check_bitrate_range(&config, bitrate); let gop = config.keyframe_interval.unwrap_or(DEFAULT_GOP as _) as i32; let ctx = EncodeContext { @@ -176,15 +170,19 @@ impl EncoderApi for HwRamEncoder { false } - fn set_quality(&mut self, quality: crate::codec::Quality) -> ResultType<()> { - let b = Self::convert_quality(&self.config.name, quality); - let mut bitrate = base_bitrate(self.config.width as _, self.config.height as _) * b / 100; + fn set_quality(&mut self, ratio: f32) -> ResultType<()> { + let mut bitrate = Self::bitrate( + &self.config.name, + self.config.width, + self.config.height, + ratio, + ); if bitrate > 0 { bitrate = Self::check_bitrate_range(&self.config, bitrate); self.encoder.set_bitrate(bitrate as _).ok(); self.bitrate = bitrate; } - self.config.quality = quality; + self.config.quality = ratio; Ok(()) } @@ -192,10 +190,6 @@ impl EncoderApi for HwRamEncoder { self.bitrate } - fn support_abr(&self) -> bool { - ["vaapi"].iter().all(|&x| !self.config.name.contains(x)) - } - fn support_changing_quality(&self) -> bool { ["vaapi"].iter().all(|&x| !self.config.name.contains(x)) } @@ -254,21 +248,35 @@ impl HwRamEncoder { RC_CBR } - pub fn convert_quality(name: &str, quality: crate::codec::Quality) -> u32 { - use crate::codec::Quality; - let quality = match quality { - Quality::Best => 150, - Quality::Balanced => 100, - Quality::Low => 50, - Quality::Custom(b) => b, - }; - let factor = if name.contains("mediacodec") { + pub fn bitrate(name: &str, width: usize, height: usize, ratio: f32) -> u32 { + Self::calc_bitrate(width, height, ratio, name.contains("h264")) + } + + pub fn calc_bitrate(width: usize, height: usize, ratio: f32, h264: bool) -> u32 { + let base = base_bitrate(width as _, height as _) as f32 * ratio; + let threshold = 2000.0; + let decay_rate = 0.001; // 1000 * 0.001 = 1 + let factor: f32 = if cfg!(target_os = "android") { // https://stackoverflow.com/questions/26110337/what-are-valid-bit-rates-to-set-for-mediacodec?rq=3 - 5 + if base > threshold { + 1.0 + 4.0 / (1.0 + (base - threshold) * decay_rate) + } else { + 5.0 + } + } else if h264 { + if base > threshold { + 1.0 + 1.0 / (1.0 + (base - threshold) * decay_rate) + } else { + 2.0 + } } else { - 1 + if base > threshold { + 1.0 + 0.5 / (1.0 + (base - threshold) * decay_rate) + } else { + 1.5 + } }; - quality * factor + (base * factor) as u32 } pub fn check_bitrate_range(_config: &HwRamEncoderConfig, bitrate: u32) -> u32 { diff --git a/libs/scrap/src/common/vpxcodec.rs b/libs/scrap/src/common/vpxcodec.rs index 11b497fb3bc1..244f38ed57b2 100644 --- a/libs/scrap/src/common/vpxcodec.rs +++ b/libs/scrap/src/common/vpxcodec.rs @@ -1,13 +1,14 @@ // https://github.com/astraw/vpx-encode // https://github.com/astraw/env-libvpx-sys // https://github.com/rust-av/vpx-rs/blob/master/src/decoder.rs +// https://github.com/chromium/chromium/blob/e7b24573bc2e06fed4749dd6b6abfce67f29052f/media/video/vpx_video_encoder.cc#L522 use hbb_common::anyhow::{anyhow, Context}; use hbb_common::log; use hbb_common::message_proto::{Chroma, EncodedVideoFrame, EncodedVideoFrames, VideoFrame}; use hbb_common::ResultType; -use crate::codec::{base_bitrate, codec_thread_num, EncoderApi, Quality}; +use crate::codec::{base_bitrate, codec_thread_num, EncoderApi}; use crate::{EncodeInput, EncodeYuvFormat, GoogleImage, Pixfmt, STRIDE_ALIGN}; use super::vpx::{vp8e_enc_control_id::*, vpx_codec_err_t::*, *}; @@ -19,9 +20,6 @@ use std::{ptr, slice}; generate_call_macro!(call_vpx, false); generate_call_ptr_macro!(call_vpx_ptr); -const DEFAULT_QP_MAX: u32 = 56; // no more than 63 -const DEFAULT_QP_MIN: u32 = 12; // no more than 63 - #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] pub enum VpxVideoCodecId { VP8, @@ -85,21 +83,11 @@ impl EncoderApi for VpxEncoder { c.kf_mode = vpx_kf_mode::VPX_KF_DISABLED; // reduce bandwidth a lot } - let (q_min, q_max, b) = Self::convert_quality(config.quality); - if q_min > 0 && q_min < q_max && q_max < 64 { - c.rc_min_quantizer = q_min; - c.rc_max_quantizer = q_max; - } else { - c.rc_min_quantizer = DEFAULT_QP_MIN; - c.rc_max_quantizer = DEFAULT_QP_MAX; - } - let base_bitrate = base_bitrate(config.width as _, config.height as _); - let bitrate = base_bitrate * b / 100; - if bitrate > 0 { - c.rc_target_bitrate = bitrate; - } else { - c.rc_target_bitrate = base_bitrate; - } + let (q_min, q_max) = Self::calc_q_values(config.quality); + c.rc_min_quantizer = q_min; + c.rc_max_quantizer = q_max; + c.rc_target_bitrate = + Self::bitrate(config.width as _, config.height as _, config.quality); // https://chromium.googlesource.com/webm/libvpx/+/refs/heads/main/vp9/common/vp9_enums.h#29 // https://chromium.googlesource.com/webm/libvpx/+/refs/heads/main/vp8/vp8_cx_iface.c#282 c.g_profile = if i444 && config.codec == VpxVideoCodecId::VP9 { @@ -212,17 +200,12 @@ impl EncoderApi for VpxEncoder { false } - fn set_quality(&mut self, quality: Quality) -> ResultType<()> { + fn set_quality(&mut self, ratio: f32) -> ResultType<()> { let mut c = unsafe { *self.ctx.config.enc.to_owned() }; - let (q_min, q_max, b) = Self::convert_quality(quality); - if q_min > 0 && q_min < q_max && q_max < 64 { - c.rc_min_quantizer = q_min; - c.rc_max_quantizer = q_max; - } - let bitrate = base_bitrate(self.width as _, self.height as _) * b / 100; - if bitrate > 0 { - c.rc_target_bitrate = bitrate; - } + let (q_min, q_max) = Self::calc_q_values(ratio); + c.rc_min_quantizer = q_min; + c.rc_max_quantizer = q_max; + c.rc_target_bitrate = Self::bitrate(self.width as _, self.height as _, ratio); call_vpx!(vpx_codec_enc_config_set(&mut self.ctx, &c)); Ok(()) } @@ -232,9 +215,6 @@ impl EncoderApi for VpxEncoder { c.rc_target_bitrate } - fn support_abr(&self) -> bool { - true - } fn support_changing_quality(&self) -> bool { true } @@ -331,30 +311,27 @@ impl VpxEncoder { } } - fn convert_quality(quality: Quality) -> (u32, u32, u32) { - match quality { - Quality::Best => (6, 45, 150), - Quality::Balanced => (12, 56, 100 * 2 / 3), - Quality::Low => (18, 56, 50), - Quality::Custom(b) => { - let (q_min, q_max) = Self::calc_q_values(b); - (q_min, q_max, b) - } - } + fn bitrate(width: u32, height: u32, ratio: f32) -> u32 { + let bitrate = base_bitrate(width, height) as f32; + (bitrate * ratio) as u32 } #[inline] - fn calc_q_values(b: u32) -> (u32, u32) { + fn calc_q_values(ratio: f32) -> (u32, u32) { + let b = (ratio * 100.0) as u32; let b = std::cmp::min(b, 200); - let q_min1: i32 = 36; + let q_min1 = 36; let q_min2 = 0; let q_max1 = 56; let q_max2 = 37; let t = b as f32 / 200.0; - let q_min: u32 = ((1.0 - t) * q_min1 as f32 + t * q_min2 as f32).round() as u32; - let q_max = ((1.0 - t) * q_max1 as f32 + t * q_max2 as f32).round() as u32; + let mut q_min: u32 = ((1.0 - t) * q_min1 as f32 + t * q_min2 as f32).round() as u32; + let mut q_max = ((1.0 - t) * q_max1 as f32 + t * q_max2 as f32).round() as u32; + + q_min = q_min.clamp(q_min2, q_min1); + q_max = q_max.clamp(q_max2, q_max1); (q_min, q_max) } @@ -415,8 +392,8 @@ pub struct VpxEncoderConfig { pub width: c_uint, /// The height (in pixels). pub height: c_uint, - /// The image quality - pub quality: Quality, + /// The bitrate ratio + pub quality: f32, /// The codec pub codec: VpxVideoCodecId, /// keyframe interval diff --git a/libs/scrap/src/common/vram.rs b/libs/scrap/src/common/vram.rs index eb3b8e1ce39e..747dbbe76db8 100644 --- a/libs/scrap/src/common/vram.rs +++ b/libs/scrap/src/common/vram.rs @@ -5,7 +5,7 @@ use std::{ }; use crate::{ - codec::{base_bitrate, enable_vram_option, EncoderApi, EncoderCfg, Quality}, + codec::{base_bitrate, enable_vram_option, EncoderApi, EncoderCfg}, hwcodec::HwCodecConfig, AdapterDevice, CodecFormat, EncodeInput, EncodeYuvFormat, Pixfmt, }; @@ -17,7 +17,7 @@ use hbb_common::{ ResultType, }; use hwcodec::{ - common::{AdapterVendor::*, DataFormat, Driver, MAX_GOP}, + common::{DataFormat, Driver, MAX_GOP}, vram::{ decode::{self, DecodeFrame, Decoder}, encode::{self, EncodeFrame, Encoder}, @@ -39,7 +39,7 @@ pub struct VRamEncoderConfig { pub device: AdapterDevice, pub width: usize, pub height: usize, - pub quality: Quality, + pub quality: f32, pub feature: FeatureContext, pub keyframe_interval: Option, } @@ -51,7 +51,6 @@ pub struct VRamEncoder { bitrate: u32, last_frame_len: usize, same_bad_len_counter: usize, - config: VRamEncoderConfig, } impl EncoderApi for VRamEncoder { @@ -61,12 +60,12 @@ impl EncoderApi for VRamEncoder { { match cfg { EncoderCfg::VRAM(config) => { - let b = Self::convert_quality(config.quality, &config.feature); - let base_bitrate = base_bitrate(config.width as _, config.height as _); - let mut bitrate = base_bitrate * b / 100; - if bitrate <= 0 { - bitrate = base_bitrate; - } + let bitrate = Self::bitrate( + config.feature.data_format, + config.width, + config.height, + config.quality, + ); let gop = config.keyframe_interval.unwrap_or(MAX_GOP as _) as i32; let ctx = EncodeContext { f: config.feature.clone(), @@ -87,7 +86,6 @@ impl EncoderApi for VRamEncoder { bitrate, last_frame_len: 0, same_bad_len_counter: 0, - config, }), Err(_) => Err(anyhow!(format!("Failed to create encoder"))), } @@ -172,9 +170,13 @@ impl EncoderApi for VRamEncoder { true } - fn set_quality(&mut self, quality: Quality) -> ResultType<()> { - let b = Self::convert_quality(quality, &self.ctx.f); - let bitrate = base_bitrate(self.ctx.d.width as _, self.ctx.d.height as _) * b / 100; + fn set_quality(&mut self, ratio: f32) -> ResultType<()> { + let bitrate = Self::bitrate( + self.ctx.f.data_format, + self.ctx.d.width as _, + self.ctx.d.height as _, + ratio, + ); if bitrate > 0 { if self.encoder.set_bitrate((bitrate) as _).is_ok() { self.bitrate = bitrate; @@ -187,10 +189,6 @@ impl EncoderApi for VRamEncoder { self.bitrate } - fn support_abr(&self) -> bool { - self.config.device.vendor_id != ADAPTER_VENDOR_INTEL as u32 - } - fn support_changing_quality(&self) -> bool { true } @@ -285,31 +283,8 @@ impl VRamEncoder { } } - pub fn convert_quality(quality: Quality, f: &FeatureContext) -> u32 { - match quality { - Quality::Best => { - if f.driver == Driver::MFX && f.data_format == DataFormat::H264 { - 200 - } else { - 150 - } - } - Quality::Balanced => { - if f.driver == Driver::MFX && f.data_format == DataFormat::H264 { - 150 - } else { - 100 - } - } - Quality::Low => { - if f.driver == Driver::MFX && f.data_format == DataFormat::H264 { - 75 - } else { - 50 - } - } - Quality::Custom(b) => b, - } + pub fn bitrate(fmt: DataFormat, width: usize, height: usize, ratio: f32) -> u32 { + crate::hwcodec::HwRamEncoder::calc_bitrate(width, height, ratio, fmt == DataFormat::H264) } pub fn set_not_use(display: usize, not_use: bool) { diff --git a/src/server/connection.rs b/src/server/connection.rs index 93e45336342f..326d128777f0 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -228,7 +228,6 @@ pub struct Connection { #[cfg(target_os = "linux")] linux_headless_handle: LinuxHeadlessHandle, closed: bool, - delay_response_instant: Instant, #[cfg(not(any(target_os = "android", target_os = "ios")))] start_cm_ipc_para: Option, auto_disconnect_timer: Option<(Instant, u64)>, @@ -376,7 +375,6 @@ impl Connection { #[cfg(target_os = "linux")] linux_headless_handle, closed: false, - delay_response_instant: Instant::now(), #[cfg(not(any(target_os = "android", target_os = "ios")))] start_cm_ipc_para: Some(StartCmIpcPara { rx_to_cm, @@ -736,7 +734,11 @@ impl Connection { }); conn.send(msg_out.into()).await; } - video_service::VIDEO_QOS.lock().unwrap().user_delay_response_elapsed(conn.inner.id(), conn.delay_response_instant.elapsed().as_millis()); + if conn.is_authed_remote_conn() { + if let Some(last_test_delay) = conn.last_test_delay { + video_service::VIDEO_QOS.lock().unwrap().user_delay_response_elapsed(id, last_test_delay.elapsed().as_millis()); + } + } } } } @@ -1877,7 +1879,6 @@ impl Connection { .user_network_delay(self.inner.id(), new_delay); self.network_delay = new_delay; } - self.delay_response_instant = Instant::now(); } } else if let Some(message::Union::SwitchSidesResponse(_s)) = msg.union { #[cfg(feature = "flutter")] @@ -3322,6 +3323,13 @@ impl Connection { session_id: self.lr.session_id, } } + + fn is_authed_remote_conn(&self) -> bool { + if let Some(id) = self.authed_conn_id.as_ref() { + return id.conn_type() == AuthConnType::Remote; + } + false + } } pub fn insert_switch_sides_uuid(id: String, uuid: uuid::Uuid) { @@ -3809,10 +3817,6 @@ mod raii { fn drop(&mut self) { let mut active_conns_lock = ALIVE_CONNS.lock().unwrap(); active_conns_lock.retain(|&c| c != self.0); - video_service::VIDEO_QOS - .lock() - .unwrap() - .on_connection_close(self.0); } } @@ -3830,6 +3834,12 @@ mod raii { _ONCE.call_once(|| { shutdown_hooks::add_shutdown_hook(connection_shutdown_hook); }); + if conn_type == AuthConnType::Remote { + video_service::VIDEO_QOS + .lock() + .unwrap() + .on_connection_open(conn_id); + } Self(conn_id, conn_type) } @@ -3927,12 +3937,20 @@ mod raii { ); } } + + pub fn conn_type(&self) -> AuthConnType { + self.1 + } } impl Drop for AuthedConnID { fn drop(&mut self) { if self.1 == AuthConnType::Remote { scrap::codec::Encoder::update(scrap::codec::EncodingUpdate::Remove(self.0)); + video_service::VIDEO_QOS + .lock() + .unwrap() + .on_connection_close(self.0); } AUTHED_CONNS.lock().unwrap().retain(|c| c.0 != self.0); let remote_count = AUTHED_CONNS diff --git a/src/server/video_qos.rs b/src/server/video_qos.rs index 5d3aeca85ea6..51939515c244 100644 --- a/src/server/video_qos.rs +++ b/src/server/video_qos.rs @@ -1,287 +1,222 @@ use super::*; -use scrap::codec::Quality; -use std::time::Duration; +use scrap::codec::{Quality, BR_BALANCED, BR_BEST, BR_SPEED}; +use std::{ + collections::VecDeque, + time::{Duration, Instant}, +}; + +/* +FPS adjust: +a. new user connected =>set to INIT_FPS +b. TestDelay receive => update user's fps according to network delay + When network delay < DELAY_THRESHOLD_150MS, set minimum fps according to image quality, and increase fps; + When network delay >= DELAY_THRESHOLD_150MS, set minimum fps according to image quality, and decrease fps; +c. second timeout / TestDelay receive => update real fps to the minimum fps from all users + +ratio adjust: +a. user set image quality => update to the maximum ratio of the latest quality +b. 3 seconds timeout => update ratio according to network delay + When network delay < DELAY_THRESHOLD_150MS, increase ratio, max 150kbps; + When network delay >= DELAY_THRESHOLD_150MS, decrease ratio; + +adjust betwen FPS and ratio: + When network delay < DELAY_THRESHOLD_150MS, fps is always higher than the minimum fps, and ratio is increasing; + When network delay >= DELAY_THRESHOLD_150MS, fps is always lower than the minimum fps, and ratio is decreasing; + +delay: + use delay minus RTT as the actual network delay +*/ + +// Constants pub const FPS: u32 = 30; pub const MIN_FPS: u32 = 1; pub const MAX_FPS: u32 = 120; -trait Percent { - fn as_percent(&self) -> u32; +pub const INIT_FPS: u32 = 15; + +// Bitrate ratio constants for different quality levels +const BR_MAX: f32 = 40.0; // 2000 * 2 / 100 +const BR_MIN: f32 = 0.2; +const BR_MIN_HIGH_RESOLUTION: f32 = 0.1; // For high resolution, BR_MIN is still too high, so we set a lower limit +const MAX_BR_MULTIPLE: f32 = 1.0; + +const HISTORY_DELAY_LEN: usize = 2; +const ADJUST_RATIO_INTERVAL: usize = 3; // Adjust quality ratio every 3 seconds +const DYNAMIC_SCREEN_THRESHOLD: usize = 2; // Allow increase quality ratio if encode more than 2 times in one second +const DELAY_THRESHOLD_150MS: u32 = 150; // 150ms is the threshold for good network condition + +#[derive(Default, Debug, Clone)] +struct UserDelay { + response_delayed: bool, + delay_history: VecDeque, + fps: Option, + rtt_calculator: RttCalculator, + quick_increase_fps_count: usize, + increase_fps_count: usize, } -impl Percent for ImageQuality { - fn as_percent(&self) -> u32 { - match self { - ImageQuality::NotSet => 0, - ImageQuality::Low => 50, - ImageQuality::Balanced => 66, - ImageQuality::Best => 100, +impl UserDelay { + fn add_delay(&mut self, delay: u32) { + self.rtt_calculator.update(delay); + if self.delay_history.len() > HISTORY_DELAY_LEN { + self.delay_history.pop_front(); } + self.delay_history.push_back(delay); } -} -#[derive(Default, Debug, Copy, Clone)] -struct Delay { - state: DelayState, - staging_state: DelayState, - delay: u32, - counter: u32, - slower_than_old_state: Option, + // Average delay minus RTT + fn avg_delay(&self) -> u32 { + let len = self.delay_history.len(); + if len > 0 { + let avg_delay = self.delay_history.iter().sum::() / len as u32; + + // If RTT is available, subtract it from average delay to get actual network latency + if let Some(rtt) = self.rtt_calculator.get_rtt() { + if avg_delay > rtt { + avg_delay - rtt + } else { + avg_delay + } + } else { + avg_delay + } + } else { + DELAY_THRESHOLD_150MS + } + } } -#[derive(Default, Debug, Copy, Clone)] +// User session data structure +#[derive(Default, Debug, Clone)] struct UserData { auto_adjust_fps: Option, // reserve for compatibility custom_fps: Option, quality: Option<(i64, Quality)>, // (time, quality) - delay: Option, - response_delayed: bool, + delay: UserDelay, record: bool, } +#[derive(Default, Debug, Clone)] +struct DisplayData { + send_counter: usize, // Number of times encode during period + support_changing_quality: bool, +} + +// Main QoS controller structure pub struct VideoQoS { fps: u32, - quality: Quality, + ratio: f32, users: HashMap, + displays: HashMap, bitrate_store: u32, - support_abr: HashMap, -} - -#[derive(PartialEq, Debug, Clone, Copy)] -enum DelayState { - Normal = 0, - LowDelay = 200, - HighDelay = 500, - Broken = 1000, -} - -impl Default for DelayState { - fn default() -> Self { - DelayState::Normal - } -} - -impl DelayState { - fn from_delay(delay: u32) -> Self { - if delay > DelayState::Broken as u32 { - DelayState::Broken - } else if delay > DelayState::HighDelay as u32 { - DelayState::HighDelay - } else if delay > DelayState::LowDelay as u32 { - DelayState::LowDelay - } else { - DelayState::Normal - } - } + adjust_ratio_instant: Instant, + abr_config: bool, + new_user_instant: Instant, } impl Default for VideoQoS { fn default() -> Self { VideoQoS { fps: FPS, - quality: Default::default(), + ratio: 1.0, users: Default::default(), + displays: Default::default(), bitrate_store: 0, - support_abr: Default::default(), + adjust_ratio_instant: Instant::now(), + abr_config: true, + new_user_instant: Instant::now(), } } } -#[derive(Debug, PartialEq, Eq)] -pub enum RefreshType { - SetImageQuality, -} - +// Basic functionality impl VideoQoS { + // Calculate seconds per frame based on current FPS pub fn spf(&self) -> Duration { Duration::from_secs_f32(1. / (self.fps() as f32)) } + // Get current FPS within valid range pub fn fps(&self) -> u32 { - if self.fps >= MIN_FPS && self.fps <= MAX_FPS { - self.fps + let fps = self.fps; + if fps >= MIN_FPS && fps <= MAX_FPS { + fps } else { FPS } } + // Store bitrate for later use pub fn store_bitrate(&mut self, bitrate: u32) { self.bitrate_store = bitrate; } + // Get stored bitrate pub fn bitrate(&self) -> u32 { self.bitrate_store } - pub fn quality(&self) -> Quality { - self.quality + // Get current bitrate ratio with bounds checking + pub fn ratio(&mut self) -> f32 { + if self.ratio < BR_MIN_HIGH_RESOLUTION || self.ratio > BR_MAX { + self.ratio = BR_BALANCED; + } + self.ratio } + // Check if any user is in recording mode pub fn record(&self) -> bool { self.users.iter().any(|u| u.1.record) } - pub fn set_support_abr(&mut self, display_idx: usize, support: bool) { - self.support_abr.insert(display_idx, support); + pub fn set_support_changing_quality(&mut self, display_idx: usize, support: bool) { + if let Some(display) = self.displays.get_mut(&display_idx) { + display.support_changing_quality = support; + } } + // Check if variable bitrate encoding is supported and enabled pub fn in_vbr_state(&self) -> bool { - Config::get_option("enable-abr") != "N" && self.support_abr.iter().all(|e| *e.1) + self.abr_config && self.displays.iter().all(|e| e.1.support_changing_quality) } +} - pub fn refresh(&mut self, typ: Option) { - // fps - let user_fps = |u: &UserData| { - // custom_fps - let mut fps = u.custom_fps.unwrap_or(FPS); - // auto adjust fps - if let Some(auto_adjust_fps) = u.auto_adjust_fps { - if fps == 0 || auto_adjust_fps < fps { - fps = auto_adjust_fps; - } - } - // delay - if let Some(delay) = u.delay { - fps = match delay.state { - DelayState::Normal => fps, - DelayState::LowDelay => fps * 3 / 4, - DelayState::HighDelay => fps / 2, - DelayState::Broken => fps / 4, - } - } - // delay response - if u.response_delayed { - if fps > MIN_FPS + 2 { - fps = MIN_FPS + 2; - } - } - return fps; - }; - let mut fps = self - .users - .iter() - .map(|(_, u)| user_fps(u)) - .filter(|u| *u >= MIN_FPS) - .min() - .unwrap_or(FPS); - if fps > MAX_FPS { - fps = MAX_FPS; - } - self.fps = fps; - - // quality - // latest image quality - let latest_quality = self - .users - .iter() - .map(|(_, u)| u.quality) - .filter(|q| *q != None) - .max_by(|a, b| a.unwrap_or_default().0.cmp(&b.unwrap_or_default().0)) - .unwrap_or_default() - .unwrap_or_default() - .1; - let mut quality = latest_quality; +// User session management +impl VideoQoS { + // Initialize new user session + pub fn on_connection_open(&mut self, id: i32) { + self.users.insert(id, UserData::default()); + self.abr_config = Config::get_option("enable-abr") != "N"; + self.new_user_instant = Instant::now(); + } - // network delay - let abr_enabled = self.in_vbr_state(); - if abr_enabled && typ != Some(RefreshType::SetImageQuality) { - // max delay - let delay = self - .users - .iter() - .map(|u| u.1.delay) - .filter(|d| d.is_some()) - .max_by(|a, b| { - (a.unwrap_or_default().state as u32).cmp(&(b.unwrap_or_default().state as u32)) - }); - let delay = delay.unwrap_or_default().unwrap_or_default().state; - if delay != DelayState::Normal { - match self.quality { - Quality::Best => { - quality = if delay == DelayState::Broken { - Quality::Low - } else { - Quality::Balanced - }; - } - Quality::Balanced => { - quality = Quality::Low; - } - Quality::Low => { - quality = Quality::Low; - } - Quality::Custom(b) => match delay { - DelayState::LowDelay => { - quality = - Quality::Custom(if b >= 150 { 100 } else { std::cmp::min(50, b) }); - } - DelayState::HighDelay => { - quality = - Quality::Custom(if b >= 100 { 50 } else { std::cmp::min(25, b) }); - } - DelayState::Broken => { - quality = - Quality::Custom(if b >= 50 { 25 } else { std::cmp::min(10, b) }); - } - DelayState::Normal => {} - }, - } - } else { - match self.quality { - Quality::Low => { - if latest_quality == Quality::Best { - quality = Quality::Balanced; - } - } - Quality::Custom(current_b) => { - if let Quality::Custom(latest_b) = latest_quality { - if current_b < latest_b / 2 { - quality = Quality::Custom(latest_b / 2); - } - } - } - _ => {} - } - } + // Clean up user session + pub fn on_connection_close(&mut self, id: i32) { + self.users.remove(&id); + if self.users.is_empty() { + *self = Default::default(); } - self.quality = quality; } pub fn user_custom_fps(&mut self, id: i32, fps: u32) { - if fps < MIN_FPS { + if fps < MIN_FPS || fps > MAX_FPS { return; } if let Some(user) = self.users.get_mut(&id) { user.custom_fps = Some(fps); - } else { - self.users.insert( - id, - UserData { - custom_fps: Some(fps), - ..Default::default() - }, - ); } - self.refresh(None); } pub fn user_auto_adjust_fps(&mut self, id: i32, fps: u32) { + if fps < MIN_FPS || fps > MAX_FPS { + return; + } if let Some(user) = self.users.get_mut(&id) { user.auto_adjust_fps = Some(fps); - } else { - self.users.insert( - id, - UserData { - auto_adjust_fps: Some(fps), - ..Default::default() - }, - ); } - self.refresh(None); } pub fn user_image_quality(&mut self, id: i32, image_quality: i32) { - // https://github.com/rustdesk/rustdesk/blob/d716e2b40c38737f1aa3f16de0dec67394a6ac68/src/server/video_service.rs#L493 - let convert_quality = |q: i32| { + let convert_quality = |q: i32| -> Quality { if q == ImageQuality::Balanced.value() { Quality::Balanced } else if q == ImageQuality::Low.value() { @@ -289,103 +224,367 @@ impl VideoQoS { } else if q == ImageQuality::Best.value() { Quality::Best } else { - let mut b = (q >> 8 & 0xFFF) * 2; - b = std::cmp::max(b, 20); - b = std::cmp::min(b, 8000); - Quality::Custom(b as u32) + let b = ((q >> 8 & 0xFFF) * 2) as f32 / 100.0; + Quality::Custom(b.clamp(BR_MIN, BR_MAX)) } }; let quality = Some((hbb_common::get_time(), convert_quality(image_quality))); if let Some(user) = self.users.get_mut(&id) { user.quality = quality; - } else { - self.users.insert( - id, - UserData { - quality, - ..Default::default() - }, - ); + // update ratio directly + self.ratio = self.latest_quality().ratio(); + } + } + + pub fn user_record(&mut self, id: i32, v: bool) { + if let Some(user) = self.users.get_mut(&id) { + user.record = v; } - self.refresh(Some(RefreshType::SetImageQuality)); } pub fn user_network_delay(&mut self, id: i32, delay: u32) { - let state = DelayState::from_delay(delay); - let debounce = 3; + let highest_fps = self.highest_fps(); + let target_ratio = self.latest_quality().ratio(); + + // For bad network, small fps means quick reaction and high quality + let (min_fps, normal_fps) = if target_ratio >= BR_BEST { + (8, 16) + } else if target_ratio >= BR_BALANCED { + (10, 20) + } else { + (12, 24) + }; + + // Calculate minimum acceptable delay-fps product + let dividend_ms = DELAY_THRESHOLD_150MS * min_fps; + + let mut adjust_ratio = false; if let Some(user) = self.users.get_mut(&id) { - if let Some(d) = &mut user.delay { - d.delay = (delay + d.delay) / 2; - let new_state = DelayState::from_delay(d.delay); - let slower_than_old_state = new_state as i32 - d.staging_state as i32; - let slower_than_old_state = if slower_than_old_state > 0 { - Some(true) - } else if slower_than_old_state < 0 { - Some(false) + let delay = delay.max(10); + let old_avg_delay = user.delay.avg_delay(); + user.delay.add_delay(delay); + let mut avg_delay = user.delay.avg_delay(); + avg_delay = avg_delay.max(10); + let mut fps = self.fps; + + // Adaptive FPS adjustment based on network delay: + if avg_delay < 50 { + user.delay.quick_increase_fps_count += 1; + let mut step = if fps < normal_fps { 1 } else { 0 }; + if user.delay.quick_increase_fps_count >= 3 { + // After 3 consecutive good samples, increase more aggressively + user.delay.quick_increase_fps_count = 0; + step = 5; + } + fps = min_fps.max(fps + step); + } else if avg_delay < 100 { + let step = if avg_delay < old_avg_delay { + if fps < normal_fps { + 1 + } else { + 0 + } } else { - None + 0 }; - if d.slower_than_old_state == slower_than_old_state { - let old_counter = d.counter; - d.counter += delay / 1000 + 1; - if old_counter < debounce && d.counter >= debounce { - d.counter = 0; - d.state = d.staging_state; - d.staging_state = new_state; - } - if d.counter % debounce == 0 { - self.refresh(None); - } + fps = min_fps.max(fps + step); + } else if avg_delay < DELAY_THRESHOLD_150MS { + fps = min_fps.max(fps); + } else { + let devide_fps = ((fps as f32) / (avg_delay as f32 / DELAY_THRESHOLD_150MS as f32)) + .ceil() as u32; + if avg_delay < 200 { + fps = min_fps.max(devide_fps); + } else if avg_delay < 300 { + fps = min_fps.min(devide_fps); + } else if avg_delay < 600 { + fps = dividend_ms / avg_delay; } else { - d.counter = 0; - d.staging_state = new_state; - d.slower_than_old_state = slower_than_old_state; + fps = (dividend_ms / avg_delay).min(devide_fps); } + } + + if avg_delay < DELAY_THRESHOLD_150MS { + user.delay.increase_fps_count += 1; } else { - user.delay = Some(Delay { - state: DelayState::Normal, - staging_state: state, - delay, - counter: 0, - slower_than_old_state: None, - }); + user.delay.increase_fps_count = 0; } - } else { - self.users.insert( - id, - UserData { - delay: Some(Delay { - state: DelayState::Normal, - staging_state: state, - delay, - counter: 0, - slower_than_old_state: None, - }), - ..Default::default() - }, - ); + if user.delay.increase_fps_count >= 3 { + // After 3 stable samples, try increasing FPS + user.delay.increase_fps_count = 0; + fps += 1; + } + + // Reset quick increase counter if network condition worsens + if avg_delay > 50 { + user.delay.quick_increase_fps_count = 0; + } + + fps = fps.clamp(MIN_FPS, highest_fps); + // first network delay message + adjust_ratio = user.delay.fps.is_none(); + user.delay.fps = Some(fps); + } + self.adjust_fps(); + if adjust_ratio { + self.adjust_ratio(false); } } pub fn user_delay_response_elapsed(&mut self, id: i32, elapsed: u128) { if let Some(user) = self.users.get_mut(&id) { - let old = user.response_delayed; - user.response_delayed = elapsed > 3000; - if old != user.response_delayed { - self.refresh(None); + user.delay.response_delayed = elapsed > 2000; + if user.delay.response_delayed { + user.delay.add_delay(elapsed as u32); + self.adjust_fps(); } } } +} - pub fn user_record(&mut self, id: i32, v: bool) { - if let Some(user) = self.users.get_mut(&id) { - user.record = v; +// Common adjust functions +impl VideoQoS { + pub fn new_display(&mut self, display_idx: usize) { + self.displays.insert(display_idx, DisplayData::default()); + } + + pub fn remove_display(&mut self, display_idx: usize) { + self.displays.remove(&display_idx); + } + + pub fn update_display_data(&mut self, display_idx: usize, send_counter: usize) { + if let Some(display) = self.displays.get_mut(&display_idx) { + display.send_counter += send_counter; + } + self.adjust_fps(); + let abr_enabled = self.in_vbr_state(); + if abr_enabled { + if self.adjust_ratio_instant.elapsed().as_secs() >= ADJUST_RATIO_INTERVAL as u64 { + let dynamic_screen = self + .displays + .iter() + .any(|d| d.1.send_counter >= ADJUST_RATIO_INTERVAL * DYNAMIC_SCREEN_THRESHOLD); + self.displays.iter_mut().for_each(|d| { + d.1.send_counter = 0; + }); + self.adjust_ratio(dynamic_screen); + } + } else { + self.ratio = self.latest_quality().ratio(); } } - pub fn on_connection_close(&mut self, id: i32) { - self.users.remove(&id); - self.refresh(None); + #[inline] + fn highest_fps(&self) -> u32 { + let user_fps = |u: &UserData| { + let mut fps = u.custom_fps.unwrap_or(FPS); + if let Some(auto_adjust_fps) = u.auto_adjust_fps { + if fps == 0 || auto_adjust_fps < fps { + fps = auto_adjust_fps; + } + } + fps + }; + + let fps = self + .users + .iter() + .map(|(_, u)| user_fps(u)) + .filter(|u| *u >= MIN_FPS) + .min() + .unwrap_or(FPS); + + fps.clamp(MIN_FPS, MAX_FPS) + } + + // Get latest quality settings from all users + pub fn latest_quality(&self) -> Quality { + self.users + .iter() + .map(|(_, u)| u.quality) + .filter(|q| *q != None) + .max_by(|a, b| a.unwrap_or_default().0.cmp(&b.unwrap_or_default().0)) + .flatten() + .unwrap_or((0, Quality::Balanced)) + .1 + } + + // Adjust quality ratio based on network delay and screen changes + fn adjust_ratio(&mut self, dynamic_screen: bool) { + // Get maximum delay from all users + let max_delay = self.users.iter().map(|u| u.1.delay.avg_delay()).max(); + let Some(max_delay) = max_delay else { + return; + }; + + let target_quality = self.latest_quality(); + let target_ratio = self.latest_quality().ratio(); + let current_ratio = self.ratio; + let current_bitrate = self.bitrate(); + + // Calculate minimum ratio for high resolution (1Mbps baseline) + let ratio_1mbps = if current_bitrate > 0 { + Some((current_ratio * 1000.0 / current_bitrate as f32).max(BR_MIN_HIGH_RESOLUTION)) + } else { + None + }; + + // Calculate ratio for adding 150kbps bandwidth + let ratio_add_150kbps = if current_bitrate > 0 { + Some((current_bitrate + 150) as f32 * current_ratio / current_bitrate as f32) + } else { + None + }; + + // Set minimum ratio based on quality mode + let min = match target_quality { + Quality::Best => { + // For Best quality, ensure minimum 1Mbps for high resolution + let mut min = BR_BEST / 2.5; + if let Some(ratio_1mbps) = ratio_1mbps { + if min > ratio_1mbps { + min = ratio_1mbps; + } + } + min.max(BR_MIN) + } + Quality::Balanced => { + let mut min = (BR_BALANCED / 2.0).min(0.4); + if let Some(ratio_1mbps) = ratio_1mbps { + if min > ratio_1mbps { + min = ratio_1mbps; + } + } + min.max(BR_MIN_HIGH_RESOLUTION) + } + Quality::Low => BR_MIN_HIGH_RESOLUTION, + Quality::Custom(_) => BR_MIN_HIGH_RESOLUTION, + }; + let max = target_ratio * MAX_BR_MULTIPLE; + + let mut v = current_ratio; + + // Adjust ratio based on network delay thresholds + if max_delay < 50 { + if dynamic_screen { + v = current_ratio * 1.15; + } + } else if max_delay < 100 { + if dynamic_screen { + v = current_ratio * 1.1; + } + } else if max_delay < DELAY_THRESHOLD_150MS { + if dynamic_screen { + v = current_ratio * 1.05; + } + } else if max_delay < 200 { + v = current_ratio * 0.95; + } else if max_delay < 300 { + v = current_ratio * 0.9; + } else if max_delay < 500 { + v = current_ratio * 0.85; + } else { + v = current_ratio * 0.8; + } + + // Limit quality increase rate for better stability + if let Some(ratio_add_150kbps) = ratio_add_150kbps { + if v > ratio_add_150kbps + && ratio_add_150kbps > current_ratio + && current_ratio >= BR_SPEED + { + v = ratio_add_150kbps; + } + } + + self.ratio = v.clamp(min, max); + self.adjust_ratio_instant = Instant::now(); + } + + // Adjust fps based on network delay and user response time + fn adjust_fps(&mut self) { + let highest_fps = self.highest_fps(); + // Get minimum fps from all users + let mut fps = self + .users + .iter() + .map(|u| u.1.delay.fps.unwrap_or(INIT_FPS)) + .min() + .unwrap_or(INIT_FPS); + + if self.users.iter().any(|u| u.1.delay.response_delayed) { + if fps > MIN_FPS + 1 { + fps = MIN_FPS + 1; + } + } + + // For new connections (within 1 second), cap fps to INIT_FPS to ensure stability + if self.new_user_instant.elapsed().as_secs() < 1 { + if fps > INIT_FPS { + fps = INIT_FPS; + } + } + + // Ensure fps stays within valid range + self.fps = fps.clamp(MIN_FPS, highest_fps); + } +} + +#[derive(Default, Debug, Clone)] +struct RttCalculator { + min_rtt: Option, // Historical minimum RTT ever observed + window_min_rtt: Option, // Minimum RTT within last 60 samples + smoothed_rtt: Option, // Smoothed RTT estimation + samples: VecDeque, // Last 60 RTT samples +} + +impl RttCalculator { + const WINDOW_SAMPLES: usize = 60; // Keep last 60 samples + const MIN_SAMPLES: usize = 10; // Require at least 10 samples + const ALPHA: f32 = 0.5; // Smoothing factor for weighted average + + /// Update RTT estimates with a new sample + pub fn update(&mut self, delay: u32) { + // 1. Update historical minimum RTT + match self.min_rtt { + Some(min_rtt) if delay < min_rtt => self.min_rtt = Some(delay), + None => self.min_rtt = Some(delay), + _ => {} + } + + // 2. Update sample window + if self.samples.len() >= Self::WINDOW_SAMPLES { + self.samples.pop_front(); + } + self.samples.push_back(delay); + + // 3. Calculate minimum RTT within the window + self.window_min_rtt = self.samples.iter().min().copied(); + + // 4. Calculate smoothed RTT + // Use weighted average if we have enough samples + if self.samples.len() >= Self::WINDOW_SAMPLES { + if let (Some(min), Some(window_min)) = (self.min_rtt, self.window_min_rtt) { + // Weighted average of historical minimum and window minimum + let new_srtt = + ((1.0 - Self::ALPHA) * min as f32 + Self::ALPHA * window_min as f32) as u32; + self.smoothed_rtt = Some(new_srtt); + } + } + } + + /// Get current RTT estimate + /// Returns None if no valid estimation is available + pub fn get_rtt(&self) -> Option { + if let Some(rtt) = self.smoothed_rtt { + return Some(rtt); + } + if self.samples.len() >= Self::MIN_SAMPLES { + if let Some(rtt) = self.min_rtt { + return Some(rtt); + } + } + None } } diff --git a/src/server/video_service.rs b/src/server/video_service.rs index 733405a37f70..5bc58da45d32 100644 --- a/src/server/video_service.rs +++ b/src/server/video_service.rs @@ -51,7 +51,7 @@ use scrap::vram::{VRamEncoder, VRamEncoderConfig}; use scrap::Capturer; use scrap::{ aom::AomEncoderConfig, - codec::{Encoder, EncoderCfg, Quality}, + codec::{Encoder, EncoderCfg}, record::{Recorder, RecorderContext}, vpxcodec::{VpxEncoderConfig, VpxVideoCodecId}, CodecFormat, Display, EncodeInput, TraitCapturer, @@ -413,9 +413,8 @@ fn run(vs: VideoService) -> ResultType<()> { c.set_gdi(); } let mut video_qos = VIDEO_QOS.lock().unwrap(); - video_qos.refresh(None); - let mut spf; - let mut quality = video_qos.quality(); + let mut spf = video_qos.spf(); + let mut quality = video_qos.ratio(); let record_incoming = config::option2bool( "allow-auto-record-incoming", &Config::get_option("allow-auto-record-incoming"), @@ -461,7 +460,7 @@ fn run(vs: VideoService) -> ResultType<()> { VIDEO_QOS .lock() .unwrap() - .set_support_abr(display_idx, encoder.support_abr()); + .set_support_changing_quality(display_idx, encoder.support_changing_quality()); log::info!("initial quality: {quality:?}"); if sp.is_option_true(OPTION_REFRESH) { @@ -489,32 +488,20 @@ fn run(vs: VideoService) -> ResultType<()> { let mut first_frame = true; let capture_width = c.width; let capture_height = c.height; + let (mut second_instant, mut send_counter) = (Instant::now(), 0); while sp.ok() { #[cfg(windows)] check_uac_switch(c.privacy_mode_id, c._capturer_privacy_mode_id)?; - - let mut video_qos = VIDEO_QOS.lock().unwrap(); - spf = video_qos.spf(); - if quality != video_qos.quality() { - log::debug!("quality: {:?} -> {:?}", quality, video_qos.quality()); - quality = video_qos.quality(); - if encoder.support_changing_quality() { - allow_err!(encoder.set_quality(quality)); - video_qos.store_bitrate(encoder.bitrate()); - } else { - if !video_qos.in_vbr_state() && !quality.is_custom() { - log::info!("switch to change quality"); - bail!("SWITCH"); - } - } - } - if client_record != video_qos.record() { - log::info!("switch due to record changed"); - bail!("SWITCH"); - } - drop(video_qos); - + check_qos( + &mut encoder, + &mut quality, + &mut spf, + client_record, + &mut send_counter, + &mut second_instant, + display_idx, + )?; if sp.is_option_true(OPTION_REFRESH) { let _ = try_broadcast_display_changed(&sp, display_idx, &c, true); log::info!("switch to refresh"); @@ -582,6 +569,7 @@ fn run(vs: VideoService) -> ResultType<()> { capture_height, )?; frame_controller.set_send(now, send_conn_ids); + send_counter += 1; } #[cfg(windows)] { @@ -640,6 +628,7 @@ fn run(vs: VideoService) -> ResultType<()> { capture_height, )?; frame_controller.set_send(now, send_conn_ids); + send_counter += 1; } } } @@ -691,6 +680,7 @@ struct Raii(usize); impl Raii { fn new(display_idx: usize) -> Self { + VIDEO_QOS.lock().unwrap().new_display(display_idx); Raii(display_idx) } } @@ -701,14 +691,14 @@ impl Drop for Raii { VRamEncoder::set_not_use(self.0, false); #[cfg(feature = "vram")] Encoder::update(scrap::codec::EncodingUpdate::Check); - VIDEO_QOS.lock().unwrap().set_support_abr(self.0, true); + VIDEO_QOS.lock().unwrap().remove_display(self.0); } } fn setup_encoder( c: &CapturerInfo, display_idx: usize, - quality: Quality, + quality: f32, client_record: bool, record_incoming: bool, last_portable_service_running: bool, @@ -737,7 +727,7 @@ fn setup_encoder( fn get_encoder_config( c: &CapturerInfo, _display_idx: usize, - quality: Quality, + quality: f32, record: bool, _portable_service: bool, ) -> EncoderCfg { @@ -1061,3 +1051,40 @@ pub fn make_display_changed_msg( msg_out.set_misc(misc); Some(msg_out) } + +fn check_qos( + encoder: &mut Encoder, + ratio: &mut f32, + spf: &mut Duration, + client_record: bool, + send_counter: &mut usize, + second_instant: &mut Instant, + display_idx: usize, +) -> ResultType<()> { + let mut video_qos = VIDEO_QOS.lock().unwrap(); + *spf = video_qos.spf(); + if *ratio != video_qos.ratio() { + *ratio = video_qos.ratio(); + if encoder.support_changing_quality() { + allow_err!(encoder.set_quality(*ratio)); + video_qos.store_bitrate(encoder.bitrate()); + } else { + // Now only vaapi doesn't support changing quality + if !video_qos.in_vbr_state() && !video_qos.latest_quality().is_custom() { + log::info!("switch to change quality"); + bail!("SWITCH"); + } + } + } + if client_record != video_qos.record() { + log::info!("switch due to record changed"); + bail!("SWITCH"); + } + if second_instant.elapsed() > Duration::from_secs(1) { + *second_instant = Instant::now(); + video_qos.update_display_data(display_idx, *send_counter); + *send_counter = 0; + } + drop(video_qos); + Ok(()) +} From 1f02bc9d3ed379e970f8ae4d4497f76adfd98d1c Mon Sep 17 00:00:00 2001 From: 21pages Date: Mon, 20 Jan 2025 23:12:00 +0800 Subject: [PATCH 525/541] bump to 1.3.7 (#10548) Signed-off-by: 21pages --- .github/workflows/flutter-build.yml | 2 +- .github/workflows/playground.yml | 2 +- Cargo.lock | 4 ++-- Cargo.toml | 2 +- appimage/AppImageBuilder-aarch64.yml | 2 +- appimage/AppImageBuilder-x86_64.yml | 2 +- flutter/pubspec.yaml | 2 +- libs/portable/Cargo.toml | 2 +- res/PKGBUILD | 2 +- res/rpm-flutter-suse.spec | 2 +- res/rpm-flutter.spec | 2 +- res/rpm.spec | 2 +- 12 files changed, 13 insertions(+), 13 deletions(-) diff --git a/.github/workflows/flutter-build.yml b/.github/workflows/flutter-build.yml index 30b75af64788..da96b9af0776 100644 --- a/.github/workflows/flutter-build.yml +++ b/.github/workflows/flutter-build.yml @@ -33,7 +33,7 @@ env: VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite" # vcpkg version: 2024.11.16 VCPKG_COMMIT_ID: "b2cb0da531c2f1f740045bfe7c4dac59f0b2b69c" - VERSION: "1.3.6" + VERSION: "1.3.7" NDK_VERSION: "r27c" #signing keys env variable checks ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}" diff --git a/.github/workflows/playground.yml b/.github/workflows/playground.yml index b0b7a57258f1..7cb1f801c529 100644 --- a/.github/workflows/playground.yml +++ b/.github/workflows/playground.yml @@ -18,7 +18,7 @@ env: VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite" # vcpkg version: 2024.11.16 VCPKG_COMMIT_ID: "b2cb0da531c2f1f740045bfe7c4dac59f0b2b69c" - VERSION: "1.3.6" + VERSION: "1.3.7" NDK_VERSION: "r26d" #signing keys env variable checks ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}" diff --git a/Cargo.lock b/Cargo.lock index 6fab0e8fa2e6..e8ed6c42e125 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5506,7 +5506,7 @@ dependencies = [ [[package]] name = "rustdesk" -version = "1.3.6" +version = "1.3.7" dependencies = [ "android-wakelock", "android_logger", @@ -5606,7 +5606,7 @@ dependencies = [ [[package]] name = "rustdesk-portable-packer" -version = "1.3.6" +version = "1.3.7" dependencies = [ "brotli", "dirs 5.0.1", diff --git a/Cargo.toml b/Cargo.toml index 28cc25cc1598..917b49a879c5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rustdesk" -version = "1.3.6" +version = "1.3.7" authors = ["rustdesk "] edition = "2021" build= "build.rs" diff --git a/appimage/AppImageBuilder-aarch64.yml b/appimage/AppImageBuilder-aarch64.yml index 1e8e39596065..a5ae4ef78c9d 100644 --- a/appimage/AppImageBuilder-aarch64.yml +++ b/appimage/AppImageBuilder-aarch64.yml @@ -18,7 +18,7 @@ AppDir: id: rustdesk name: rustdesk icon: rustdesk - version: 1.3.6 + version: 1.3.7 exec: usr/share/rustdesk/rustdesk exec_args: $@ apt: diff --git a/appimage/AppImageBuilder-x86_64.yml b/appimage/AppImageBuilder-x86_64.yml index 646113d4a495..25889d5b7d0c 100644 --- a/appimage/AppImageBuilder-x86_64.yml +++ b/appimage/AppImageBuilder-x86_64.yml @@ -18,7 +18,7 @@ AppDir: id: rustdesk name: rustdesk icon: rustdesk - version: 1.3.6 + version: 1.3.7 exec: usr/share/rustdesk/rustdesk exec_args: $@ apt: diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index a64c61415ed4..3871449996e0 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # 1.1.9-1 works for android, but for ios it becomes 1.1.91, need to set it to 1.1.9-a.1 for iOS, will get 1.1.9.1, but iOS store not allow 4 numbers -version: 1.3.6+55 +version: 1.3.7+56 environment: sdk: '^3.1.0' diff --git a/libs/portable/Cargo.toml b/libs/portable/Cargo.toml index b9982bce79e0..5553c718811f 100644 --- a/libs/portable/Cargo.toml +++ b/libs/portable/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rustdesk-portable-packer" -version = "1.3.6" +version = "1.3.7" edition = "2021" description = "RustDesk Remote Desktop" diff --git a/res/PKGBUILD b/res/PKGBUILD index ab97225a3c4d..f27fd6d638dc 100644 --- a/res/PKGBUILD +++ b/res/PKGBUILD @@ -1,5 +1,5 @@ pkgname=rustdesk -pkgver=1.3.6 +pkgver=1.3.7 pkgrel=0 epoch= pkgdesc="" diff --git a/res/rpm-flutter-suse.spec b/res/rpm-flutter-suse.spec index 347ecd989e48..d56838d3c3fd 100644 --- a/res/rpm-flutter-suse.spec +++ b/res/rpm-flutter-suse.spec @@ -1,5 +1,5 @@ Name: rustdesk -Version: 1.3.6 +Version: 1.3.7 Release: 0 Summary: RPM package License: GPL-3.0 diff --git a/res/rpm-flutter.spec b/res/rpm-flutter.spec index b6a25ac55fa5..771c8a12e7ff 100644 --- a/res/rpm-flutter.spec +++ b/res/rpm-flutter.spec @@ -1,5 +1,5 @@ Name: rustdesk -Version: 1.3.6 +Version: 1.3.7 Release: 0 Summary: RPM package License: GPL-3.0 diff --git a/res/rpm.spec b/res/rpm.spec index 90c57e673675..eb4a9a7ad381 100644 --- a/res/rpm.spec +++ b/res/rpm.spec @@ -1,5 +1,5 @@ Name: rustdesk -Version: 1.3.6 +Version: 1.3.7 Release: 0 Summary: RPM package License: GPL-3.0 From 0eba939cd6d0d465ba81d7c4e52d580273d90ac3 Mon Sep 17 00:00:00 2001 From: 21pages Date: Tue, 21 Jan 2025 16:57:07 +0800 Subject: [PATCH 526/541] fix windows crash (#10562) Signed-off-by: 21pages --- Cargo.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index e8ed6c42e125..23c75024446e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1584,7 +1584,7 @@ dependencies = [ [[package]] name = "default_net" version = "0.1.0" -source = "git+https://github.com/rustdesk-org/default_net#a831d47bcacb4615b394968287697924a8f62be1" +source = "git+https://github.com/rustdesk-org/default_net#c400b0bbf49a987129796221fbc41a8a385b812e" dependencies = [ "anyhow", "regex", From d04756ad702da3da57cb333841efe8501c84a69a Mon Sep 17 00:00:00 2001 From: 21pages Date: Tue, 21 Jan 2025 17:09:24 +0800 Subject: [PATCH 527/541] replace self-hosted arm64 linux with ubuntu-22.04-arm (#10555) https://github.blog/changelog/2025-01-16-linux-arm64-hosted-runners-now-available-for-free-in-public-repositories-public-preview/ Signed-off-by: 21pages --- .github/workflows/flutter-build.yml | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/.github/workflows/flutter-build.yml b/.github/workflows/flutter-build.yml index da96b9af0776..bf9469c0e572 100644 --- a/.github/workflows/flutter-build.yml +++ b/.github/workflows/flutter-build.yml @@ -1398,7 +1398,7 @@ jobs: arch: aarch64, target: aarch64-unknown-linux-gnu, distro: ubuntu18.04, - on: [self-hosted, Linux, ARM64], + on: ubuntu-22.04-arm, deb_arch: arm64, vcpkg-triplet: arm64-linux, } @@ -1411,13 +1411,15 @@ jobs: core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || ''); - name: Maximize build space - if: ${{ matrix.job.arch == 'x86_64' }} run: | sudo rm -rf /opt/ghc sudo rm -rf /usr/local/lib/android sudo rm -rf /usr/share/dotnet sudo apt-get update -y - sudo apt-get install -y nasm qemu-user-static + sudo apt-get install -y nasm + if [[ "${{ matrix.job.arch }}" == "x86_64" ]]; then + sudo apt-get install -y qemu-user-static + fi - name: Checkout source code uses: actions/checkout@v4 @@ -1713,7 +1715,6 @@ jobs: build-rustdesk-linux-sciter: if: ${{ inputs.upload-artifact }} - needs: build-rustdesk-linux # not for dep, just make it run later for parallelism runs-on: ${{ matrix.job.on }} name: build-rustdesk-linux-sciter ${{ matrix.job.target }} strategy: @@ -1734,7 +1735,7 @@ jobs: - { arch: armv7, target: armv7-unknown-linux-gnueabihf, - on: [self-hosted, Linux, ARM64], + on: ubuntu-22.04-arm, distro: ubuntu18.04-rustdesk, deb_arch: armhf, sciter_arch: arm32, @@ -2010,7 +2011,7 @@ jobs: target: aarch64-unknown-linux-gnu, # try out newer flatpak since error of "error: Nothing matches org.freedesktop.Platform in remote flathub" distro: ubuntu22.04, - on: [self-hosted, Linux, ARM64], + on: ubuntu-22.04-arm, arch: aarch64, suffix: "", } From ec3ba5be8e5055c73ff76546c6b639df93da13e1 Mon Sep 17 00:00:00 2001 From: Vasyl Gello Date: Wed, 22 Jan 2025 01:26:03 +0000 Subject: [PATCH 528/541] Fix issues spotted during 1.3.7 fdroid build (#10570) * bridge.yml: Explicitly install cargo-expand of certain version @linsui spotted this trying to fix the build failure of 1.3.7 on f-droid: https://gitlab.com/fdroid/fdroiddata/-/merge_requests/18766 * flutter-build.yml: drop workarounds for flutter 3.13 @fufesou has removed them from build_fdroid.sh in #10040 but forgot to remove them in main flutter_build.yml. flutter 3.13 is not used anymore, and those who want to build the old version using flutter 3.13 can happily check out the appropriate commit from Git history. * Bump vcpkg baseline to 2025.01.13 @linsui addressed the missing vcpkg-tools.json file inside vcpkg revision (microsoft side, not rustdesk's!) by updating the vcpkg baseline. --- .github/workflows/bridge.yml | 2 ++ .github/workflows/flutter-build.yml | 24 ++---------------------- flutter/build_fdroid.sh | 5 +++++ 3 files changed, 9 insertions(+), 22 deletions(-) diff --git a/.github/workflows/bridge.yml b/.github/workflows/bridge.yml index 5e38de4a9376..2d5affeef520 100644 --- a/.github/workflows/bridge.yml +++ b/.github/workflows/bridge.yml @@ -6,6 +6,7 @@ on: workflow_call: env: + CARGO_EXPAND_VERSION: "1.0.95" FLUTTER_VERSION: "3.22.3" FLUTTER_RUST_BRIDGE_VERSION: "1.80.1" RUST_VERSION: "1.75" # https://github.com/rustdesk/rustdesk/discussions/7503 @@ -75,6 +76,7 @@ jobs: - name: Install flutter rust bridge deps shell: bash run: | + cargo install cargo-expand --version ${{ env.CARGO_EXPAND_VERSION }} --locked cargo install flutter_rust_bridge_codegen --version ${{ env.FLUTTER_RUST_BRIDGE_VERSION }} --features "uuid" --locked pushd flutter && sed -i -e 's/extended_text: 14.0.0/extended_text: 13.0.0/g' pubspec.yaml && flutter pub get && popd diff --git a/.github/workflows/flutter-build.yml b/.github/workflows/flutter-build.yml index bf9469c0e572..bb43c39d3ecf 100644 --- a/.github/workflows/flutter-build.yml +++ b/.github/workflows/flutter-build.yml @@ -31,8 +31,8 @@ env: FLUTTER_ELINUX_VERSION: "3.16.9" TAG_NAME: "${{ inputs.upload-tag }}" VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite" - # vcpkg version: 2024.11.16 - VCPKG_COMMIT_ID: "b2cb0da531c2f1f740045bfe7c4dac59f0b2b69c" + # vcpkg version: 2025.01.13 + VCPKG_COMMIT_ID: "6f29f12e82a8293156836ad81cc9bf5af41fe836" VERSION: "1.3.7" NDK_VERSION: "r27c" #signing keys env variable checks @@ -1043,16 +1043,6 @@ jobs: prefix-key: rustdesk-lib-cache-android # TODO: drop '-android' part after caches are invalidated key: ${{ matrix.job.target }} - - name: fix android for flutter 3.13 - if: ${{ env.ANDROID_FLUTTER_VERSION == '3.13.9' }} - run: | - cd flutter - sed -i 's/uni_links_desktop/#uni_links_desktop/g' pubspec.yaml - sed -i 's/extended_text: .*/extended_text: 11.1.0/' pubspec.yaml - flutter pub get - cd lib - find . | grep dart | xargs sed -i 's/textScaler: TextScaler.linear(\(.*\)),/textScaleFactor: \1,/g' - - name: Build rustdesk lib env: ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }} @@ -1295,16 +1285,6 @@ jobs: name: librustdesk.so.i686-linux-android path: ./flutter/android/app/src/main/jniLibs/x86 - - name: fix android for flutter 3.13 - if: ${{ env.ANDROID_FLUTTER_VERSION == '3.13.9' }} - run: | - cd flutter - sed -i 's/uni_links_desktop/#uni_links_desktop/g' pubspec.yaml - sed -i 's/extended_text: .*/extended_text: 11.1.0/' pubspec.yaml - flutter pub get - cd lib - find . | grep dart | xargs sed -i 's/textScaler: TextScaler.linear(\(.*\)),/textScaleFactor: \1,/g' - - name: Build rustdesk shell: bash env: diff --git a/flutter/build_fdroid.sh b/flutter/build_fdroid.sh index 40fe3c3c3513..ecfb444efea5 100755 --- a/flutter/build_fdroid.sh +++ b/flutter/build_fdroid.sh @@ -150,6 +150,10 @@ prebuild) # Flutter used to compile Flutter<->Rust bridge files + CARGO_EXPAND_VERSION="$(yq -r \ + .env.CARGO_EXPAND_VERSION \ + .github/workflows/bridge.yml)" + FLUTTER_BRIDGE_VERSION="$(yq -r \ .env.FLUTTER_VERSION \ .github/workflows/bridge.yml)" @@ -239,6 +243,7 @@ prebuild) cargo install \ cargo-expand \ + --version "${CARGO_EXPAND_VERSION}" \ --locked cargo install flutter_rust_bridge_codegen \ --version "${FLUTTER_RUST_BRIDGE_VERSION}" \ From da80f3352ad32af8a9ea92cb943cdacd6f7186bd Mon Sep 17 00:00:00 2001 From: 21pages Date: Wed, 22 Jan 2025 20:27:00 +0800 Subject: [PATCH 529/541] fix vaapi create 2 times at first (#10576) Signed-off-by: 21pages --- Cargo.lock | 2 +- src/server/video_qos.rs | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 23c75024446e..de75588adc4e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1584,7 +1584,7 @@ dependencies = [ [[package]] name = "default_net" version = "0.1.0" -source = "git+https://github.com/rustdesk-org/default_net#c400b0bbf49a987129796221fbc41a8a385b812e" +source = "git+https://github.com/rustdesk-org/default_net#78f8f70cd85151a3a2c4a3230d80d5272703c02e" dependencies = [ "anyhow", "regex", diff --git a/src/server/video_qos.rs b/src/server/video_qos.rs index 51939515c244..108af9f4498b 100644 --- a/src/server/video_qos.rs +++ b/src/server/video_qos.rs @@ -117,7 +117,7 @@ impl Default for VideoQoS { fn default() -> Self { VideoQoS { fps: FPS, - ratio: 1.0, + ratio: BR_BALANCED, users: Default::default(), displays: Default::default(), bitrate_store: 0, @@ -327,7 +327,8 @@ impl VideoQoS { user.delay.fps = Some(fps); } self.adjust_fps(); - if adjust_ratio { + if adjust_ratio && !cfg!(target_os = "linux") { + //Reduce the possibility of vaapi being created twice self.adjust_ratio(false); } } @@ -412,6 +413,9 @@ impl VideoQoS { // Adjust quality ratio based on network delay and screen changes fn adjust_ratio(&mut self, dynamic_screen: bool) { + if !self.in_vbr_state() { + return; + } // Get maximum delay from all users let max_delay = self.users.iter().map(|u| u.1.delay.avg_delay()).max(); let Some(max_delay) = max_delay else { From 80f759c1edd901039e9ba9c1a2710be1b9180c60 Mon Sep 17 00:00:00 2001 From: bjoernp <54497210+bjoernp116@users.noreply.github.com> Date: Thu, 23 Jan 2025 06:22:25 +0100 Subject: [PATCH 530/541] norwegian translation (#10579) Signed-off-by: bjoernp116 --- README.md | 2 +- docs/CODE_OF_CONDUCT-NO.md | 125 ++++++++++++++++++++++++++ docs/CONTRIBUTING-NO.md | 46 ++++++++++ docs/DEVCONTAINER-NO.md | 14 +++ docs/README-NO.md | 177 +++++++++++++++++++++++++++++++++++++ docs/SECURITY-NO.md | 9 ++ 6 files changed, 372 insertions(+), 1 deletion(-) create mode 100644 docs/CODE_OF_CONDUCT-NO.md create mode 100644 docs/CONTRIBUTING-NO.md create mode 100644 docs/DEVCONTAINER-NO.md create mode 100644 docs/README-NO.md create mode 100644 docs/SECURITY-NO.md diff --git a/README.md b/README.md index cc555270ac85..22cd390625e7 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ DockerStructureSnapshot
    - [Українська] | [česky] | [中文] | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt] | [Dansk] | [Ελληνικά] | [Türkçe]
    + [Українська] | [česky] | [中文] | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt] | [Dansk] | [Ελληνικά] | [Türkçe] | [Norsk
    We need your help to translate this README, RustDesk UI and RustDesk Doc to your native language

    diff --git a/docs/CODE_OF_CONDUCT-NO.md b/docs/CODE_OF_CONDUCT-NO.md new file mode 100644 index 000000000000..baefda0519ac --- /dev/null +++ b/docs/CODE_OF_CONDUCT-NO.md @@ -0,0 +1,125 @@ + +# Atferdskodeks for bidragsyterpaktern + +## Hva Vi Står For + +Vi som medlemer, bidragere, og ledere står for å skape ett hat-fritt felleskap, +uansett alder, kroppstørrelse, synlig eller usynlige funksjonsnedsettninger, +etnesitet, kjønns karaktertrekk, kjønnsidentitet, kunnskapsnivå, utdanning, +sosial-økonomisk status, nasjonalitet, utsende, rase, religion, eller seksual +identitet og orientasjon. + +Vi står for åpen, velkommende, mangfold, inklusiv og sunn oppførsel i vårt felleskap. + +## Våre Standarer + +Eksempler på oppførsel som hjelper ett positivt felleskap inkluderer: + +* Vise empati og vennlighet mot andre mennesker +* Være respektfull ovenfor ulike meninger, synspunkter og erfaringer +* Gi og ta konstruktiv kritikk i beste mening +* Akseptere ansvar og unskylde seg for de som er utsatt av våre feil, + og lære av disse +* Fokusere på det som er best ikke bare for individer, men for felleskapet + +Eksempler på uakseptabel oppførsel inkluderer: + +* Bruk av seksualisert språk eller bilder, og seksual oppmerksomhet. +* Troll-ene, fornermende og nedsettende kommentarer, og personlig eller politiske angrep +* Offentlig eller privat trakassering +* Publisering av andres private informasjon, sånn som bosteds- og epost-addresser, + uten deres godskjenning. +* Andre rettningslinjer som kan bli sett på som upassende i en profesjonell setting. + +## Håndhevingsansvar + +Felleskapets ledere har ansvar for å klarifisere og håndheve våre standarer av +akseptert oppførsel og vill ta rimelige og rettferdige handliger som respons på +oppførsel de anser som upassende, truende, fornermende eller skadelig. + +Felleskapets ledere har retten og ansvaret til å fjerne, redigere, eller avslå +kommentarer, commits, kode, wiki endringer, issues, og andre birag som ikke +samsvarer med disse etiske rettningslinjene, og vill kommunisere grunner for +moderatorenes valg når passende. + +## Omfang + +Disse etiske rettningslinjene gjelder innenfor alle platformene til felleskapet, og +de gjelder også når ett individ representerer felleskapet på offentlige medier. +Eksempler på representasjon av vårt felleskap inkluderer bruke av offisielle e-mail +addresser, publisering gjennom en offisiell sosial media bruker, eller oppførsel som en +utpekt representant på digitale og fysiske arrangsjemanger. + +## Håndheving + +Hendelser av misbruk, trakasserende eller på noen måte uakseptert oppførsel kann +bli raportert til felleskapets ledere med ansvar for håndheving på +[info@rustdesk.com](mailto:info@rustdesk.com). +All tilbakemelding vill bli sett gjennom og investigert rettferdig så fort som mulig. + +Alle felleskapets ledere er obligert til å respektere privatlivet og sikkerhetet ovenfor +den som raporterer en hendelse. + +## Håndhevings Guide + +Felleskapets ledere vill følge disse Rettningslinjene for sammfunspåvirkning med +tanke på konsekvenser for en handling de anser i brudd med disse etiske rettningslinjene: + +### 1. Korreksjon + +**Sammfunspåvirkning**: Bruk av upassende språk eller annen oppførsel ansett som +uprofesjonelt eller uvelkommen i dette felleskapet. + +**Konsekvens**: En privat, skrevet advarsel fra en leder av felleskapet, som +klarifiserer grunnlaget til hvorfor denne oppførselen var upassende. En offentlig +unskyldning kan bli forespurt. + +### 2. Advarsel + +**Sammfunspåvirkning**: Ett brudd på en singulær hendelse eller en serie handlinger. + +**Konsekvens**: En advarsel med konsekvenser for kontinuerende oppførsel. Ingen +interaksjon med individene involvert, inkluderer uoppfordret interaksjoner med +de som håndhever disse etiske rettningslinjene, er tillat for en spesifisert tidsperiode. +Dette inkluderer å unngå interaksjoner i felleskapets platformer, samt eksterne +kanaler, som f.eks sosial media. Brudd av disse vilkårene kan føre til midlertidig +eller permanent bannlysning. + +### 3. Midlertidig Bannlysning + +**Sammfunspåvirkning**: Ett særiøst brudd på felleskapets standarer, inkludert +vedvarende upassende oppførsel. + +**Konsekvens**: En midlertidig bannlysning fra noen som helst interaksjon eller +offentlig kommunikasjon med felleskapet for en spesifisert tidsperiode. Ingen +interaksjon med individene involvert, inkluderer uoppfordret interaksjoner med +de som håndhever disse etiske rettningslinjene, er tillat for denne perioden. +Brudd på disse vilkårene kan føre til permanent bannlysning. + +### 4. Permanent Bannlysning + +**Sammfunspåvirkning**: Demonstasjon av mønster i brudd på felleskapets standarer, +inklusivt vedvarende upassende oppførsel, trakassering av ett individ, eller +aggresjon mot eller nedsettelse av grupper individer. + +**Konsekvens**: En permanent bannlysning fra alle offentlige interaksjoner i +felleskapet + +## Attribusjon + +Disse etiske rettningslinjene er adaptert fra [Contributor Covenant][homepage], +versjon 2.0, tilgjengelig ved +[https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0]. + +Sammfunspåvirknings guid inspirert av +[Mozilla's code of conduct enforcement ladder][Mozilla CoC]. + +For svar til vanlige spørsmål angående disse etiske rettningslinjene, se FAQ på +[https://www.contributor-covenant.org/faq][FAQ]. Oversettelse tilgjengelig +ved [https://www.contributor-covenant.org/translations][translations]. + +[homepage]: https://www.contributor-covenant.org +[v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html +[Mozilla CoC]: https://github.com/mozilla/diversity +[FAQ]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations diff --git a/docs/CONTRIBUTING-NO.md b/docs/CONTRIBUTING-NO.md new file mode 100644 index 000000000000..89a574563275 --- /dev/null +++ b/docs/CONTRIBUTING-NO.md @@ -0,0 +1,46 @@ +# Bidrag til RustDesk + +RustDesk er åpene for bidrag fra alle. Her er reglene for de som har lyst til å +hjelpe oss: + +## Bidrag + +Bidrag til RustDesk eller deres avhengigheter burde være i form av GitHub pull requests. +Hver pull request vill bli sett igjennom av en kjerne bidrager (noen med autoritet til +å godkjenne endringene) og enten bli sendt til main treet eller respondert med +tilbakemelding på endringer som er nødvendig. Alle bidrag burde følge dette formate +også de fra kjerne bidragere. + +Om du ønsker å jobbe på en issue må du huske å gjøre krav på den først. Dette +kann gjøres ved å kommentere på den GitHub issue-en du ønsker å jobbe på. +Dette er for å hindre duplikat innsats på samme problem. + +## Pull Request Sjekkliste + +- Lag en gren fra master grenen og, hvis det er nødvendig, rebase den til den nåværende + master grenen før du sender inn din pull request. Hvis ikke dette gjøres på rent + vis vill du bli spurt om å rebase dine endringer. + +- Commits burde være så små som mulig, samtidig som de må være korrekt uavhenging av hverandre + (hver commit burde kompilere og bestå tester). + +- Commits burde være akkopaniert med en Developer Certificate of Origin + (http://developercertificate.org), som indikerer att du (og din arbeidsgiver + i det tilfellet) godkjenner å bli knyttet til vilkårene av [prosjekt lisensen](../LICENCE). + Ved bruk av git er dette `-s` opsjonen til `git commit`. + +- Hvis dine endringer ikke blir sett eller hvis du trenger en spesefik person til + å se på dem kan du @-svare en med autoritet til å godkjenne dine endringer. + Dette kann gjøres i en pull request, en kommentar eller via epost på [email](mailto:info@rustdesk.com). + +- Legg til tester relevant til en fikset bug eller en ny tilgjengelighet. + +For spesefike git instruksjoner, se [GitHub workflow 101](https://github.com/servo/servo/wiki/GitHub-workflow). + +## Oppførsel + +https://github.com/rustdesk/rustdesk/blob/master/docs/CODE_OF_CONDUCT.md + +## Kommunikasjon + +RustDesk bidragere burker [Discord](https://discord.gg/nDceKgxnkV). diff --git a/docs/DEVCONTAINER-NO.md b/docs/DEVCONTAINER-NO.md new file mode 100644 index 000000000000..1d944ed5d6c6 --- /dev/null +++ b/docs/DEVCONTAINER-NO.md @@ -0,0 +1,14 @@ + +Etter start av devcontainer i docker konteineren, blir en linux binærfil i debug modus laget. + +Nå tilbyr devcontainer linux og android builds i både debug og release modus. + +Under er tabellen over kommandoer som kan kjøres fra rot-direktive for kreasjon av spesefike builds. + +Kommando|Build Type|Modus +-|-|-| +`.devcontainer/build.sh --debug linux`|Linux|debug +`.devcontainer/build.sh --release linux`|Linux|release +`.devcontainer/build.sh --debug android`|android-arm64|debug +`.devcontainer/build.sh --release android`|android-arm64|release + diff --git a/docs/README-NO.md b/docs/README-NO.md new file mode 100644 index 000000000000..dbe4ddd2deb5 --- /dev/null +++ b/docs/README-NO.md @@ -0,0 +1,177 @@ +

    + RustDesk - Your remote desktop
    + Servere • + Build • + Docker • + Struktur • + Snapshot
    + [Українська] | [česky] | [中文] | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt] | [Dansk] | [Ελληνικά] | [Türkçe] | [Norsk
    + Vi trenger din hjelp til å oversette denne README-en, RustDesk UI og RustDesk Doc tid ditt morsmål +

    + +Snakk med oss: [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) + +Enda en annen fjernstyrt desktop programvare, skrevet i Rust. Virker rett ut av pakken, ingen konfigurasjon nødvendig. Du har full kontroll over din data, uten beskymring for sikkerhet. Du kan bruke vår rendezvous_mediator/relay server, [sett opp din egen](https://rustdesk.com/server), eller [skriv din egen rendezvous_mediator/relay server](https://github.com/rustdesk/rustdesk-server-demo). + +![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png) + +RustDesk er velkommen for bidrag fra alle. Se [CONTRIBUTING.md](docs/CONTRIBUTING-NO.md) for hjelp med oppstart. + +[**FAQ**](https://github.com/rustdesk/rustdesk/wiki/FAQ) + +[**BINARY NEDLASTING**](https://github.com/rustdesk/rustdesk/releases) + +[**NIGHTLY BUILD**](https://github.com/rustdesk/rustdesk/releases/tag/nightly) + +[Få det på F-Droid](https://f-droid.org/en/packages/com.carriez.flutter_hbb) +[Få det på Flathub](https://flathub.org/apps/com.rustdesk.RustDesk) + +## Avhengigheter + +Desktop versjoner bruker Flutter eller Sciter (avviklet) for GUI, denne veiledningen er bare for Sciter, grunnet att det er letter og en mer venlig start. Skjekk ut vår [CI](https://github.com/rustdesk/rustdesk/blob/master/.github/workflows/flutter-build.yml) for bygging av Flutter versjonen. + +Venligst last ned Sciters dynamiske bibliotek selv. + +[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) + +## Rå steg for bygging + +- Klargjør ditt Rust development env og C++ build env + +- Installer [vcpkg](https://github.com/microsoft/vcpkg), og koriger `VCPKG_ROOT` env vaiabelen + + - 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 + +- Kjør `cargo run` + +## [Bygg](https://rustdesk.com/docs/en/dev/build/) + +## Hvordan Bygge til Linux + +### Ubuntu 18 (Debian 10) + +```sh +sudo apt install -y zip g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev \ + libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake make \ + libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libpam0g-dev +``` + +### openSUSE Tumbleweed + +```sh +sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel pam-devel +``` + +### Fedora 28 (CentOS 8) + +```sh +sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel gstreamer1-devel gstreamer1-plugins-base-devel pam-devel +``` + +### Arch (Manjaro) + +```sh +sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pipewire +``` + +### Installer vcpkg + +```sh +git clone https://github.com/microsoft/vcpkg +cd vcpkg +git checkout 2023.04.15 +cd .. +vcpkg/bootstrap-vcpkg.sh +export VCPKG_ROOT=$HOME/vcpkg +vcpkg/vcpkg install libvpx libyuv opus aom +``` + +### Fiks libvpx (For Fedora) + +```sh +cd vcpkg/buildtrees/libvpx/src +cd * +./configure +sed -i 's/CFLAGS+=-I/CFLAGS+=-fPIC -I/g' Makefile +sed -i 's/CXXFLAGS+=-I/CXXFLAGS+=-fPIC -I/g' Makefile +make +cp libvpx.a $HOME/vcpkg/installed/x64-linux/lib/ +cd +``` + +### Bygg + +```sh +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh +source $HOME/.cargo/env +git clone https://github.com/rustdesk/rustdesk +cd rustdesk +mkdir -p target/debug +wget https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so +mv libsciter-gtk.so target/debug +VCPKG_ROOT=$HOME/vcpkg cargo run +``` + +## Hvordan bygge med Docker + +Start med å klone repositoret og bygg Docker konteineren: + +```sh +git clone https://github.com/rustdesk/rustdesk +cd rustdesk +docker build -t "rustdesk-builder" . +``` + +Deretter, hver gang du trenger å bygge applikasjonen, kjør følgene kommando: + +```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 +``` + +Det kan ta lengere tid før avhengighetene blir bufret første gang du bygger, senere bygg er raskere. Hvis du trenger å spesifisere forkjellige argumenter til bygge kommandoen, kan du gjøre det på slutten av kommandoen ved `` feltet. For eksempel, hvis du ville bygge en optimalisert release versjon, ville du kjørt kommandoen over fulgt `--release`. Den kjørbare filen vill være tilgjengelig i mål direktive på ditt system, og kan bli kjørt med: + +```sh +target/debug/rustdesk +``` + +Eller, hvis du kjører ett release program: + +```sh +target/release/rustdesk +``` + +Venligst pass på att du kjører disse kommandoene fra roten av RustDesk repositoret, eller kan det hende att applikasjon ikke finner de riktige ressursene. Pass også på att andre cargo subkommandoer som for eksempel `install` eller `run` ikke støttes med denne metoden da de vill installere eller kjøre programmet i konteineren istedet for verten. + +## Fil Struktur + +- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: video kodek, configurasjon, tcp/udp innpakning, protobuf, fs funksjon for fil overføring, og noen andre verktøy funksjoner +- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: skjermfangst +- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: platform spesefik keyboard/mus kontroll +- **[libs/clipboard](https://github.com/rustdesk/rustdesk/tree/master/libs/clipboard)**: fil kopi og innliming implementasjon for Windows, Linux, macOS. +- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: foreldret Sciter UI (avviklet) +- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: lyd/utklippstavle/input/video tjenester, og internett tilkobling +- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: start en peer tilkobling +- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Kommunikasjon med [rustdesk-server](https://github.com/rustdesk/rustdesk-server), vent på direkte fjernstyring (TCP hulling) eller vidresendt tilkobling +- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: platform spesefik kode +- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: Flutter kode for desktop og mobil +- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: JavaScript for Flutter nettsted klient + +## Skjermbilder + +![Tilkoblings Manager](https://github.com/rustdesk/rustdesk/assets/28412477/db82d4e7-c4bc-4823-8e6f-6af7eadf7651) + +![Koble til Windows PC](https://github.com/rustdesk/rustdesk/assets/28412477/9baa91e9-3362-4d06-aa1a-7518edcbd7ea) + +![Fil Overføring](https://github.com/rustdesk/rustdesk/assets/28412477/39511ad3-aa9a-4f8c-8947-1cce286a46ad) + +![TCP Tunneling](https://github.com/rustdesk/rustdesk/assets/28412477/78e8708f-e87e-4570-8373-1360033ea6c5) + diff --git a/docs/SECURITY-NO.md b/docs/SECURITY-NO.md new file mode 100644 index 000000000000..1f8dcb411bdc --- /dev/null +++ b/docs/SECURITY-NO.md @@ -0,0 +1,9 @@ +# Sikkerhets Rettningslinjer + +## Reportering av en Sårbarhet + +Vi verdsetter pris på sikkerhet for prosjektet høyt. Og oppmunterer alle brukere til å rapportere sårbarheter de oppdager til oss. +Om du finner en sikkerhets sårbarhet i RustDesk prosjektet, venligst raportere det ansvarsfult ved å sende oss en email til info@rustdesk.com. + +På dette tidspunktet har vi ingen bug dusør program. Vi er ett lite team som prøver å løse ett stort problem. Vi trenger att du raporterer alle sårbarhetene +annsvarfult så vi kan fortsettte å bygge ett en sikker applikasjon for hele felleskapet. From 1b49d49df2f02c7e7c1832404e9cbc015c21ff9f Mon Sep 17 00:00:00 2001 From: RustDesk <71636191+rustdesk@users.noreply.github.com> Date: Thu, 23 Jan 2025 13:23:20 +0800 Subject: [PATCH 531/541] Update README.md (#10586) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 22cd390625e7..1fcb65ff3af6 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ DockerStructureSnapshot
    - [Українська] | [česky] | [中文] | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt] | [Dansk] | [Ελληνικά] | [Türkçe] | [Norsk
    + [Українська] | [česky] | [中文] | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt] | [Dansk] | [Ελληνικά] | [Türkçe] | [Norsk]
    We need your help to translate this README, RustDesk UI and RustDesk Doc to your native language

    From e4f00361f67f443847546f5127545652a08030b3 Mon Sep 17 00:00:00 2001 From: RustDesk <71636191+rustdesk@users.noreply.github.com> Date: Thu, 23 Jan 2025 13:24:14 +0800 Subject: [PATCH 532/541] Update README.md (#10587) --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 1fcb65ff3af6..1c4d6be4afe9 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,5 @@

    RustDesk - Your remote desktop
    - ServersBuildDockerStructure • From d656ae29567bd9afa3e354f121e397b94d972826 Mon Sep 17 00:00:00 2001 From: Y-Ploni <7353755@gmail.com> Date: Fri, 24 Jan 2025 09:09:36 +0200 Subject: [PATCH 533/541] Update he.rs (#10594) --- src/lang/he.rs | 196 ++++++++++++++++++++++++------------------------- 1 file changed, 98 insertions(+), 98 deletions(-) diff --git a/src/lang/he.rs b/src/lang/he.rs index d877f0226820..a5099f487612 100644 --- a/src/lang/he.rs +++ b/src/lang/he.rs @@ -1,16 +1,16 @@ lazy_static::lazy_static! { pub static ref T: std::collections::HashMap<&'static str, &'static str> = [ - ("Status", ""), + ("Status", "מצב"), ("Your Desktop", ""), ("desk_tip", "ניתן לגשת לשולחן העבודה שלך עם מזהה וסיסמה זו."), - ("Password", ""), - ("Ready", ""), + ("Password", "סיסמא"), + ("Ready", "מוכן"), ("Established", ""), ("connecting_status", "מתחבר לרשת RustDesk..."), ("Enable service", ""), - ("Start service", ""), - ("Service is running", ""), + ("Start service", "התחל שירות"), + ("Service is running", "השירות פעיל"), ("Service is not running", ""), ("not_ready_status", "לא מוכן. בדוק את החיבור שלך"), ("Control Remote Desktop", ""), @@ -20,7 +20,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Address book", ""), ("Confirmation", ""), ("TCP tunneling", ""), - ("Remove", ""), + ("Remove", "הסר"), ("Refresh random password", ""), ("Set your own password", ""), ("Enable keyboard/mouse", ""), @@ -35,21 +35,21 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Export server configuration successfully", ""), ("Invalid server configuration", ""), ("Clipboard is empty", ""), - ("Stop service", ""), + ("Stop service", "עצור שירות"), ("Change ID", ""), ("Your new ID", ""), ("length %min% to %max%", ""), ("starts with a letter", ""), ("allowed characters", ""), ("id_change_tip", "מותרים רק תווים a-z, A-Z, 0-9 ו_ (קו תחתון). האות הראשונה חייבת להיות a-z, A-Z. אורך בין 6 ל-16."), - ("Website", ""), - ("About", ""), + ("Website", "דף הבית"), + ("About", "אודות"), ("Slogan_tip", "נוצר בלב בעולם הזה הכאוטי!"), ("Privacy Statement", ""), - ("Mute", ""), + ("Mute", "השתק"), ("Build Date", "תאריך בנייה"), ("Version", ""), - ("Home", ""), + ("Home", "בית"), ("Audio Input", "קלט שמע"), ("Enhancements", ""), ("Hardware Codec", "קודק חומרה"), @@ -63,18 +63,18 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("server_not_support", "עדיין לא נתמך על ידי השרת"), ("Not available", ""), ("Too frequent", ""), - ("Cancel", ""), - ("Skip", ""), - ("Close", ""), - ("Retry", ""), - ("OK", ""), + ("Cancel", "ביטול"), + ("Skip", "דלג"), + ("Close", "סגור"), + ("Retry", "נזה שוב"), + ("OK", "אישור"), ("Password Required", "נדרשת סיסמה"), ("Please enter your password", ""), ("Remember password", ""), ("Wrong Password", "סיסמה שגויה"), ("Do you want to enter again?", ""), ("Connection Error", "שגיאת חיבור"), - ("Error", ""), + ("Error", "שגיאה"), ("Reset by the peer", ""), ("Connecting...", ""), ("Connection in progress. Please wait.", ""), @@ -82,20 +82,20 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Login Error", "שגיאת התחברות"), ("Successful", ""), ("Connected, waiting for image...", ""), - ("Name", ""), - ("Type", ""), + ("Name", "שם"), + ("Type", "סוג"), ("Modified", ""), - ("Size", ""), + ("Size", "גודל"), ("Show Hidden Files", "הצג קבצים נסתרים"), ("Receive", ""), - ("Send", ""), + ("Send", "שלח"), ("Refresh File", "רענן קובץ"), - ("Local", ""), + ("Local", "מקומי"), ("Remote", ""), ("Remote Computer", "מחשב מרוחק"), ("Local Computer", "מחשב מקומי"), ("Confirm Delete", "אשר מחיקה"), - ("Delete", ""), + ("Delete", "מחק"), ("Properties", ""), ("Multi Select", "בחירה מרובה"), ("Select All", "בחר הכל"), @@ -108,10 +108,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Do this for all conflicts", ""), ("This is irreversible!", ""), ("Deleting", ""), - ("files", ""), + ("files", "קבצים"), ("Waiting", ""), - ("Finished", ""), - ("Speed", ""), + ("Finished", "הסתיים"), + ("Speed", "מהירות"), ("Custom Image Quality", "איכות תמונה מותאמת אישית"), ("Privacy mode", ""), ("Block user input", ""), @@ -125,14 +125,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Good image quality", ""), ("Balanced", ""), ("Optimize reaction time", ""), - ("Custom", ""), + ("Custom", "מותאם אישית"), ("Show remote cursor", ""), ("Show quality monitor", ""), ("Disable clipboard", ""), ("Lock after session end", ""), ("Insert Ctrl + Alt + Del", ""), ("Insert Lock", "הוסף נעילה"), - ("Refresh", ""), + ("Refresh", "רענן"), ("ID does not exist", ""), ("Failed to connect to rendezvous server", ""), ("Please try later", ""), @@ -153,7 +153,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("config_acc", "כדי לשלוט מרחוק בשולחן העבודה שלך, עליך להעניק ל-RustDesk הרשאות \"נגישות\"."), ("config_screen", "כדי לגשת מרחוק לשולחן העבודה שלך, עליך להעניק ל-RustDesk הרשאות \"הקלטת מסך\"."), ("Installing ...", ""), - ("Install", ""), + ("Install", "התקן"), ("Installation", ""), ("Installation Path", "נתיב התקנה"), ("Create start menu shortcuts", ""), @@ -161,14 +161,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("agreement_tip", "על ידי התחלת ההתקנה, אתה מקבל את הסכם הרישיון."), ("Accept and Install", "קבל והתקן"), ("End-user license agreement", ""), - ("Generating ...", ""), + ("Generating ...", "יוצר ..."), ("Your installation is lower version.", ""), ("not_close_tcp_tip", "אל תסגור חלון זה בזמן שאתה משתמש במנהרה"), ("Listening ...", ""), ("Remote Host", "מארח מרוחק"), ("Remote Port", "פורט מרוחק"), ("Action", ""), - ("Add", ""), + ("Add", "הוסף"), ("Local Port", "פורט מקומי"), ("Local Address", "כתובת מקומית"), ("Change Local Port", "שנה פורט מקומי"), @@ -191,7 +191,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable RDP session sharing", ""), ("Auto Login", "התחברות אוטומטית (תקפה רק אם הגדרת \"נעל לאחר סיום הסשן\")"), ("Enable direct IP access", ""), - ("Rename", ""), + ("Rename", "שנה שם"), ("Space", ""), ("Create desktop shortcut", ""), ("Change Path", "שנה נתיב"), @@ -203,9 +203,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Reboot required", ""), ("Unsupported display server", ""), ("x11 expected", ""), - ("Port", ""), - ("Settings", ""), - ("Username", ""), + ("Port", "יציאה"), + ("Settings", "הגדרות"), + ("Username", "שם משתמש"), ("Invalid port", ""), ("Closed manually by the peer", ""), ("Enable remote configuration modification", ""), @@ -220,10 +220,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Verification code", ""), ("verification_tip", "קוד אימות נשלח לכתובת הדוא\"ל הרשומה, הזן את קוד האימות כדי להמשיך בהתחברות."), ("Logout", ""), - ("Tags", ""), - ("Search ID", ""), + ("Tags", "תגים"), + ("Search ID", "חפש מזהה"), ("whitelist_sep", "מופרד על ידי פסיק, נקודה פסיק, רווחים או שורה חדשה"), - ("Add ID", ""), + ("Add ID", "הוסף מזהה"), ("Add Tag", "הוסף תג"), ("Unselect all tags", ""), ("Network error", ""), @@ -236,14 +236,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Favorites", ""), ("Add to Favorites", "הוסף למועדפים"), ("Remove from Favorites", "הסר מהמועדפים"), - ("Empty", ""), + ("Empty", "ריק"), ("Invalid folder name", ""), ("Socks5 Proxy", "פרוקסי Socks5"), ("Socks5/Http(s) Proxy", "פרוקסי Socks5/Http(s)"), ("Discovered", ""), ("install_daemon_tip", "לצורך הפעלה בעת הפעלת המחשב, עליך להתקין שירות מערכת."), ("Remote ID", ""), - ("Paste", ""), + ("Paste", "הדבק"), ("Paste here?", ""), ("Are you sure to close the connection?", "האם אתה בטוח שברצונך לסגור את החיבור?"), ("Download new version", ""), @@ -265,11 +265,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Canvas Zoom", "זום בד"), ("Reset canvas", ""), ("No permission of file transfer", ""), - ("Note", ""), + ("Note", "הערה"), ("Connection", ""), ("Share Screen", "שיתוף מסך"), - ("Chat", ""), - ("Total", ""), + ("Chat", "צ'אט"), + ("Total", "הכל"), ("items", ""), ("Selected", ""), ("Screen Capture", "לכידת מסך"), @@ -291,10 +291,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Account", ""), ("Overwrite", ""), ("This file exists, skip or overwrite this file?", ""), - ("Quit", ""), - ("Help", ""), - ("Failed", ""), - ("Succeeded", ""), + ("Quit", "צא"), + ("Help", "עזרה"), + ("Failed", "נכשל"), + ("Succeeded", "הצליח"), ("Someone turns on privacy mode, exit", ""), ("Unsupported", ""), ("Peer denied", ""), @@ -322,7 +322,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("remote_restarting_tip", "המכשיר המרוחק מתחיל מחדש, אנא סגור את תיבת ההודעה הזו והתחבר מחדש עם סיסמה קבועה לאחר זמן מה"), ("Copied", ""), ("Exit Fullscreen", "יציאה ממסך מלא"), - ("Fullscreen", ""), + ("Fullscreen", "מסך מלא"), ("Mobile Actions", "פעולות ניידות"), ("Select Monitor", "בחר מסך"), ("Control Actions", "פעולות בקרה"), @@ -338,24 +338,24 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Insecure Connection", "חיבור לא מאובטח"), ("Scale original", ""), ("Scale adaptive", ""), - ("General", ""), - ("Security", ""), - ("Theme", ""), + ("General", "כללי"), + ("Security", "אבטחה"), + ("Theme", "ערכת נושא"), ("Dark Theme", "ערכת נושא כהה"), ("Light Theme", "ערכת נושא בהירה"), - ("Dark", ""), - ("Light", ""), + ("Dark", "כהה"), + ("Light", "בהיר"), ("Follow System", "עקוב אחר המערכת"), ("Enable hardware codec", ""), ("Unlock Security Settings", "פתח הגדרות אבטחה"), ("Enable audio", ""), ("Unlock Network Settings", "פתח הגדרות רשת"), - ("Server", ""), + ("Server", "שרת"), ("Direct IP Access", "גישה ישירה ל-IP"), - ("Proxy", ""), - ("Apply", ""), + ("Proxy", "פרוקסי"), + ("Apply", "החל"), ("Disconnect all devices?", ""), - ("Clear", ""), + ("Clear", "נקה"), ("Audio Input Device", "מכשיר קלט שמע"), ("Use IP Whitelisting", "השתמש ברשימת לבנה של IP"), ("Network", ""), @@ -365,7 +365,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Directory", ""), ("Automatically record incoming sessions", ""), ("Automatically record outgoing sessions", ""), - ("Change", ""), + ("Change", "שנה"), ("Start session recording", ""), ("Stop session recording", ""), ("Enable recording session", ""), @@ -376,7 +376,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please wait for confirmation of UAC...", ""), ("elevated_foreground_window_tip", "החלון הנוכחי של שולחן העבודה המרוחק דורש הרשאה גבוהה יותר לפעולה, לכן אי אפשר להשתמש בעכבר ובמקלדת באופן זמני. תוכל לבקש מהמשתמש המרוחק למזער את החלון הנוכחי, או ללחוץ על כפתור ההגבהה בחלון ניהול החיבור. כדי להימנע מבעיה זו, מומלץ להתקין את התוכנה במכשיר המרוחק."), ("Disconnected", ""), - ("Other", ""), + ("Other", "אחר"), ("Confirm before closing multiple tabs", ""), ("Keyboard Settings", "הגדרות מקלדת"), ("Full Access", "גישה מלאה"), @@ -386,8 +386,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("JumpLink", "הצג"), ("Please Select the screen to be shared(Operate on the peer side).", "אנא בחר את המסך לשיתוף (פעולה בצד העמית)."), ("Show RustDesk", ""), - ("This PC", ""), - ("or", ""), + ("This PC", "מחשב זה"), + ("or", "או"), ("Continue with", ""), ("Elevate", ""), ("Zoom cursor", ""), @@ -403,10 +403,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("hide_cm_tip", "אפשר הסתרה רק אם מקבלים סשנים דרך סיסמה ומשתמשים בסיסמה קבועה"), ("wayland_experiment_tip", "תמיכה ב-Wayland נמצאת בשלב ניסיוני, אנא השתמש ב-X11 אם אתה זקוק לגישה לא מלווה."), ("Right click to select tabs", ""), - ("Skipped", ""), + ("Skipped", "דולג"), ("Add to address book", ""), - ("Group", ""), - ("Search", ""), + ("Group", "קבוצה"), + ("Search", "חפש"), ("Closed manually by web console", ""), ("Local keyboard type", ""), ("Select local keyboard type", ""), @@ -415,7 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("config_input", "כדי לשלוט בשולחן העבודה המרוחק באמצעות מקלדת, עליך להעניק ל-RustDesk הרשאות \"מעקב אחרי קלט\"."), ("config_microphone", "כדי לדבר מרחוק, עליך להעניק ל-RustDesk הרשאות \"הקלטת שמע\"."), ("request_elevation_tip", "ניתן גם לבקש הגבהה אם יש מישהו בצד המרוחק."), - ("Wait", ""), + ("Wait", "המתן"), ("Elevation Error", "שגיאת הגבהה"), ("Ask the remote user for authentication", ""), ("Choose this if the remote account is administrator", ""), @@ -434,30 +434,30 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Strong", ""), ("Switch Sides", "החלף צדדים"), ("Please confirm if you want to share your desktop?", ""), - ("Display", ""), + ("Display", "תצוגה"), ("Default View Style", "סגנון תצוגה ברירת מחדל"), ("Default Scroll Style", "סגנון גלילה ברירת מחדל"), ("Default Image Quality", "איכות תמונה ברירת מחדל"), ("Default Codec", "קודק ברירת מחדל"), ("Bitrate", ""), - ("FPS", ""), - ("Auto", ""), + ("FPS", "FPS"), + ("Auto", "אוטומטי"), ("Other Default Options", "אפשרויות ברירת מחדל אחרות"), - ("Voice call", ""), + ("Voice call", "שיחה קולית"), ("Text chat", ""), ("Stop voice call", ""), ("relay_hint_tip", "ייתכן שלא ניתן להתחבר ישירות; ניתן לנסות להתחבר דרך ריליי. בנוסף, אם ברצונך להשתמש בריליי בניסיון הראשון שלך, תוכל להוסיף את הסיומת \"/r\" למזהה או לבחור באפשרות \"התחבר תמיד דרך ריליי\" בכרטיס של הסשנים האחרונים אם קיים."), ("Reconnect", ""), - ("Codec", ""), + ("Codec", "קודק"), ("Resolution", ""), ("No transfers in progress", ""), ("Set one-time password length", ""), ("RDP Settings", "הגדרות RDP"), - ("Sort by", ""), + ("Sort by", "מיין לפי"), ("New Connection", "חיבור חדש"), ("Restore", ""), - ("Minimize", ""), - ("Maximize", ""), + ("Minimize", "הקטן"), + ("Maximize", "הגדל"), ("Your Device", "המכשיר שלך"), ("empty_recent_tip", "אופס, אין סשנים אחרונים!\nהגיע הזמן לתכנן חדש."), ("empty_favorite_tip", "עדיין אין עמיתים מועדפים?\nבוא נמצא מישהו להתחבר אליו ונוסיף אותו למועדפים!"), @@ -483,19 +483,19 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("no_desktop_text_tip", "אנא התקן שולחן עבודה GNOME"), ("No need to elevate", ""), ("System Sound", "צליל מערכת"), - ("Default", ""), - ("New RDP", ""), + ("Default", "ברירת מחדל"), + ("New RDP", "RDP חדש"), ("Fingerprint", ""), ("Copy Fingerprint", "העתק טביעת אצבע"), ("no fingerprints", "אין טביעות אצבע"), ("Select a peer", ""), ("Select peers", ""), - ("Plugins", ""), - ("Uninstall", ""), - ("Update", ""), - ("Enable", ""), - ("Disable", ""), - ("Options", ""), + ("Plugins", "תוספים"), + ("Uninstall", "הסר"), + ("Update", "עדכן"), + ("Enable", "פועל"), + ("Disable", "כבוי"), + ("Options", "אפשרויות"), ("resolution_original_tip", "רזולוציה מקורית"), ("resolution_fit_local_tip", "התאם לרזולוציה מקומית"), ("resolution_custom_tip", "רזולוציה מותאמת אישית"), @@ -505,12 +505,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("clipboard_wait_response_timeout_tip", "המתנה לתגובת העתקה הסתיימה בזמן."), ("Incoming connection", ""), ("Outgoing connection", ""), - ("Exit", ""), - ("Open", ""), + ("Exit", "צא"), + ("Open", "פתח"), ("logout_tip", "האם אתה בטוח שברצונך להתנתק?"), ("Service", ""), - ("Start", ""), - ("Stop", ""), + ("Start", "התחל"), + ("Stop", "עצור"), ("exceed_max_devices", "הגעת למספר המקסימלי של מכשירים שניתן לנהל."), ("Sync with recent sessions", ""), ("Sort tags", ""), @@ -520,10 +520,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Already exists", ""), ("Change Password", "שנה סיסמה"), ("Refresh Password", "רענן סיסמה"), - ("ID", ""), + ("ID", "מזהה"), ("Grid View", "תצוגת רשת"), ("List View", "תצוגת רשימה"), - ("Select", ""), + ("Select", "בחר"), ("Toggle Tags", "החלף תגיות"), ("pull_ab_failed_tip", "נכשל ברענון ספר הכתובות"), ("push_ab_failed_tip", "נכשל בסנכרון ספר הכתובות לשרת"), @@ -539,8 +539,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("scam_text1", "אם אתה בשיחת טלפון עם מישהו שאינך מכיר ואינך סומך עליו שביקש ממך להשתמש ב-RustDesk ולהתחיל את השירות, אל תמשיך ונתק מיד."), ("scam_text2", "סביר להניח שמדובר בהונאה שמנסה לגנוב ממך כסף או מידע פרטי אחר."), ("Don't show again", ""), - ("I Agree", ""), - ("Decline", ""), + ("I Agree", "אני מסכים"), + ("Decline", "דחה"), ("Timeout in minutes", ""), ("auto_disconnect_option_tip", "סגור באופן אוטומטי סשנים נכנסים במקרה של חוסר פעילות של המשתמש"), ("Connection failed due to inactivity", "התנתקות אוטומטית בגלל חוסר פעילות"), @@ -549,7 +549,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("pull_group_failed_tip", "נכשל ברענון קבוצה"), ("Filter by intersection", ""), ("Remove wallpaper during incoming sessions", ""), - ("Test", ""), + ("Test", "בדיקה"), ("display_is_plugged_out_msg", "המסך הופסק, החלף למסך הראשון."), ("No displays", ""), ("Open in new window", ""), @@ -559,7 +559,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change view", ""), ("Big tiles", ""), ("Small tiles", ""), - ("List", ""), + ("List", "רשימה"), ("Virtual display", ""), ("Plug out all", ""), ("True color (4:4:4)", ""), @@ -575,7 +575,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Swap control-command key", ""), ("swap-left-right-mouse", "החלף בין כפתור העכבר השמאלי לימני"), ("2FA code", "קוד אימות דו-שלבי"), - ("More", ""), + ("More", "עוד"), ("enable-2fa-title", "הפעל אימות דו-שלבי"), ("enable-2fa-desc", "אנא הגדר כעת את האפליקציה שלך לאימות. תוכל להשתמש באפליקציית אימות כגון Authy, Microsoft או Google Authenticator בטלפון או במחשב שלך.\n\nסרוק את קוד ה-QR עם האפליקציה שלך והזן את הקוד שהאפליקציה מציגה כדי להפעיל את אימות הדו-שלבי."), ("wrong-2fa-code", "לא ניתן לאמת את הקוד. בדוק שהקוד והגדרות הזמן המקומיות נכונות"), @@ -620,8 +620,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("During controlled", ""), ("During service is on", ""), ("Capture screen using DirectX", ""), - ("Back", ""), - ("Apps", ""), + ("Back", "חזור"), + ("Apps", "אפליקציות"), ("Volume up", ""), ("Volume down", ""), ("Power", ""), @@ -630,16 +630,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("enable-bot-desc", ""), ("cancel-2fa-confirm-tip", ""), ("cancel-bot-confirm-tip", ""), - ("About RustDesk", ""), + ("About RustDesk", "אודות RustDesk"), ("Send clipboard keystrokes", ""), ("network_error_tip", ""), ("Unlock with PIN", ""), ("Requires at least {} characters", ""), ("Wrong PIN", ""), - ("Set PIN", ""), + ("Set PIN", "הגדר PIN"), ("Enable trusted devices", ""), ("Manage trusted devices", ""), - ("Platform", ""), + ("Platform", "פלטורמה"), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), ("Parent directory", ""), @@ -649,7 +649,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Authentication Required", ""), ("Authenticate", ""), ("web_id_input_tip", ""), - ("Download", ""), + ("Download", "הורדה"), ("Upload folder", ""), ("Upload files", ""), ("Clipboard is synchronized", ""), From 7aa459266943d87b88d9e9d876f09a323a6df540 Mon Sep 17 00:00:00 2001 From: Theofanis Sarmidis <126983335+tsarmis@users.noreply.github.com> Date: Sat, 25 Jan 2025 10:39:16 +0200 Subject: [PATCH 534/541] Update and fixes el.rs (#10600) --- src/lang/el.rs | 74 +++++++++++++++++++++++++------------------------- 1 file changed, 37 insertions(+), 37 deletions(-) diff --git a/src/lang/el.rs b/src/lang/el.rs index 0b78aa685603..2979c1aaad56 100644 --- a/src/lang/el.rs +++ b/src/lang/el.rs @@ -146,9 +146,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Set Password", "Ορίστε κωδικό πρόσβασης"), ("OS Password", "Κωδικός πρόσβασης λειτουργικού συστήματος"), ("install_tip", "Λόγω UAC, το RustDesk ενδέχεται να μην λειτουργεί σωστά σε ορισμένες περιπτώσεις. Για να αποφύγετε το UAC, κάντε κλικ στο κουμπί παρακάτω για να εγκαταστήσετε το RustDesk στο σύστημα"), - ("Click to upgrade", "Πιέστε για αναβάθμιση"), - ("Click to download", "Πιέστε για λήψη"), - ("Click to update", "Πιέστε για ενημέρωση"), + ("Click to upgrade", "Αναβάθμιση τώρα"), + ("Click to download", "Λήψη τώρα"), + ("Click to update", "Ενημέρωση τώρα"), ("Configure", "Διαμόρφωση"), ("config_acc", "Για τον απομακρυσμένο έλεγχο του υπολογιστή σας, πρέπει να εκχωρήσετε δικαιώματα πρόσβασης στο RustDesk."), ("config_screen", "Για να αποκτήσετε απομακρυσμένη πρόσβαση στον υπολογιστή σας, πρέπει να εκχωρήσετε το δικαίωμα RustDesk \"Screen Capture\"."), @@ -511,7 +511,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Service", "Υπηρεσία"), ("Start", "Έναρξη"), ("Stop", "Διακοπή"), - ("exceed_max_devices", "Έχετε ξεπεράσει το μέγιστο όριο αποθηκευμένων συνδέσεων"), + ("exceed_max_devices", "Υπέρβαση μέγιστου ορίου αποθηκευμένων συνδέσεων"), ("Sync with recent sessions", "Συγχρονισμός των πρόσφατων συνεδριών"), ("Sort tags", "Ταξινόμηση ετικετών"), ("Open connection in new tab", "Άνοιγμα σύνδεσης σε νέα καρτέλα"), @@ -592,19 +592,19 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Personal", "Προσωπικό"), ("Owner", "Ιδιοκτήτης"), ("Set shared password", "Ορίστε κοινόχρηστο κωδικό πρόσβασης"), - ("Exist in", ""), + ("Exist in", "Υπάρχει στο"), ("Read-only", "Μόνο για ανάγνωση"), - ("Read/Write", ""), + ("Read/Write", "Ανάγνωση/Εγγραφή"), ("Full Control", "Πλήρης Έλεγχος"), ("share_warning_tip", "Τα παραπάνω πεδία είναι κοινόχρηστα και ορατά σε άλλους."), - ("Everyone", ""), - ("ab_web_console_tip", ""), + ("Everyone", "Όλοι"), + ("ab_web_console_tip", "Περισσότερα στην κονσόλα web"), ("allow-only-conn-window-open-tip", "Να επιτρέπεται η σύνδεση μόνο εάν το παράθυρο RustDesk είναι ανοιχτό"), ("no_need_privacy_mode_no_physical_displays_tip", "Δεν υπάρχουν φυσικές οθόνες, δεν χρειάζεται να χρησιμοποιήσετε τη λειτουργία απορρήτου."), - ("Follow remote cursor", ""), - ("Follow remote window focus", ""), - ("default_proxy_tip", ""), - ("no_audio_input_device_tip", ""), + ("Follow remote cursor", "Παρακολούθηση απομακρυσμένου κέρσορα"), + ("Follow remote window focus", "Παρακολούθηση απομακρυσμένου ενεργού παραθύρου"), + ("default_proxy_tip", "Προκαθορισμένο πρωτόκολλο Socks5 στην πόρτα 1080"), + ("no_audio_input_device_tip", "Δεν βρέθηκε συσκευή εισόδου ήχου."), ("Incoming", "Εισερχόμενη"), ("Outgoing", "Εξερχόμενη"), ("Clear Wayland screen selection", ""), @@ -619,11 +619,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Never", "Ποτέ"), ("During controlled", "Κατα την διάρκεια απομακρυσμένου ελέγχου"), ("During service is on", "Κατα την εκκίνηση της υπηρεσίας Rustdesk"), - ("Capture screen using DirectX", ""), + ("Capture screen using DirectX", "Καταγραφή οθόνης με χρήση DirectX"), ("Back", "Πίσω"), ("Apps", "Εφαρμογές"), - ("Volume up", ""), - ("Volume down", ""), + ("Volume up", "Αύξηση έντασης"), + ("Volume down", "Μείωση έντασης"), ("Power", ""), ("Telegram bot", ""), ("enable-bot-tip", "Εάν ενεργοποιήσετε αυτήν τη δυνατότητα, μπορείτε να λάβετε τον κωδικό 2FA από το bot σας. Μπορεί επίσης να λειτουργήσει ως ειδοποίηση σύνδεσης."), @@ -631,30 +631,30 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("cancel-2fa-confirm-tip", "Είστε βέβαιοι ότι θέλετε να ακυρώσετε το 2FA;"), ("cancel-bot-confirm-tip", "Είστε βέβαιοι ότι θέλετε να ακυρώσετε το Telegram bot;"), ("About RustDesk", ""), - ("Send clipboard keystrokes", ""), - ("network_error_tip", ""), - ("Unlock with PIN", ""), - ("Requires at least {} characters", ""), - ("Wrong PIN", ""), - ("Set PIN", ""), - ("Enable trusted devices", ""), - ("Manage trusted devices", ""), - ("Platform", ""), - ("Days remaining", ""), - ("enable-trusted-devices-tip", ""), - ("Parent directory", ""), - ("Resume", ""), - ("Invalid file name", ""), + ("Send clipboard keystrokes", "Αποστολή προχείρου με πλήκτρα συντόμευσης"), + ("network_error_tip", "Ελέγξτε τη σύνδεσή σας στο δίκτυο και, στη συνέχεια, κάντε κλικ στην επανάληψη."), + ("Unlock with PIN", "Ξεκλείδωμα με PIN"), + ("Requires at least {} characters", "Απαιτούνται τουλάχιστον {} χαρακτήρες"), + ("Wrong PIN", "Λάθος PIN"), + ("Set PIN", "Ορισμός PIN"), + ("Enable trusted devices", "Ενεργοποίηση αξιόπιστων συσκευών"), + ("Manage trusted devices", "Διαχείριση αξιόπιστων συσκευών"), + ("Platform", "Πλατφόρμα"), + ("Days remaining", "Ημέρες που απομένουν"), + ("enable-trusted-devices-tip", "Παράβλεψη επαλήθευσης 2FA σε αξιόπιστες συσκευές."), + ("Parent directory", "Γονικός φάκελος"), + ("Resume", "Συνέχεια"), + ("Invalid file name", "Μη έγκυρο όνομα αρχείου"), ("one-way-file-transfer-tip", ""), - ("Authentication Required", ""), - ("Authenticate", ""), + ("Authentication Required", "Απαιτείται έλεγχος ταυτότητας"), + ("Authenticate", "Πιστοποίηση"), ("web_id_input_tip", ""), ("Download", ""), - ("Upload folder", ""), - ("Upload files", ""), - ("Clipboard is synchronized", ""), - ("Update client clipboard", ""), - ("Untagged", ""), - ("new-version-of-{}-tip", ""), + ("Upload folder", "Μεταφόρτωση φακέλου"), + ("Upload files", "Μεταφόρτωση αρχείων"), + ("Clipboard is synchronized", "Το πρόχειρο έχει συγχρονιστεί"), + ("Update client clipboard", "Ενημέρωση απομακρισμένου προχείρου"), + ("Untagged", "Χωρίς ετικέτα"), + ("new-version-of-{}-tip", "Υπάρχει διαθέσιμη νέα έκδοση του {}"), ].iter().cloned().collect(); } From fc2e27bcf02adff710ad958e0613dbf5d592aea9 Mon Sep 17 00:00:00 2001 From: XLion Date: Sun, 26 Jan 2025 14:18:26 +0800 Subject: [PATCH 535/541] Create dependabot.yml (#10593) --- .github/dependabot.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000000..56258e4e0a90 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +version: 2 +updates: + - package-ecosystem: "gitsubmodule" + directory: "/" + target-branch: "master" + schedule: + interval: "daily" + commit-message: + prefix: "Git submodule" + labels: + - "dependencies" From f08cb0412d32634475d75ac7e2a66a83dedad8b8 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Sun, 26 Jan 2025 19:39:38 +0800 Subject: [PATCH 536/541] fix: windows, dll, pre-loading attack (#10608) Signed-off-by: fufesou --- Cargo.lock | 2 +- libs/scrap/src/dxgi/mag.rs | 2 +- ...10.disable-loading-DLLs-from-app-dir.patch | 31 ++++++++ res/vcpkg/ffmpeg/portfile.cmake | 1 + src/core_main.rs | 5 +- src/flutter.rs | 41 +++++++++-- src/platform/windows.rs | 73 +++++++++++++++++-- 7 files changed, 138 insertions(+), 17 deletions(-) create mode 100644 res/vcpkg/ffmpeg/patch/0010.disable-loading-DLLs-from-app-dir.patch diff --git a/Cargo.lock b/Cargo.lock index de75588adc4e..4b762259505d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3077,7 +3077,7 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hwcodec" version = "0.7.1" -source = "git+https://github.com/rustdesk-org/hwcodec#c4d6b1c5c4ddc7548868306004cf5d4eb614a36f" +source = "git+https://github.com/rustdesk-org/hwcodec#0ea7e709d3c48bb6446e33a9cc8fd0e0da5709b9" dependencies = [ "bindgen 0.59.2", "cc", diff --git a/libs/scrap/src/dxgi/mag.rs b/libs/scrap/src/dxgi/mag.rs index 923606a82776..cc36b3c2308a 100644 --- a/libs/scrap/src/dxgi/mag.rs +++ b/libs/scrap/src/dxgi/mag.rs @@ -133,7 +133,7 @@ impl MagInterface { s.lib_handle = LoadLibraryExA( lib_file_name_c.as_ptr() as _, NULL, - LOAD_WITH_ALTERED_SEARCH_PATH, + LOAD_LIBRARY_SEARCH_SYSTEM32, ); if s.lib_handle.is_null() { return Err(Error::new( diff --git a/res/vcpkg/ffmpeg/patch/0010.disable-loading-DLLs-from-app-dir.patch b/res/vcpkg/ffmpeg/patch/0010.disable-loading-DLLs-from-app-dir.patch new file mode 100644 index 000000000000..18da50b446c0 --- /dev/null +++ b/res/vcpkg/ffmpeg/patch/0010.disable-loading-DLLs-from-app-dir.patch @@ -0,0 +1,31 @@ +diff --git a/compat/w32dlfcn.h b/compat/w32dlfcn.h +index ac20e83..1e83aa6 100644 +--- a/compat/w32dlfcn.h ++++ b/compat/w32dlfcn.h +@@ -76,6 +76,7 @@ static inline HMODULE win32_dlopen(const char *name) + if (!name_w) + goto exit; + namelen = wcslen(name_w); ++ /* + // Try local directory first + path = get_module_filename(NULL); + if (!path) +@@ -91,6 +92,7 @@ static inline HMODULE win32_dlopen(const char *name) + path = new_path; + wcscpy(path + pathlen + 1, name_w); + module = LoadLibraryExW(path, NULL, LOAD_WITH_ALTERED_SEARCH_PATH); ++ */ + if (module == NULL) { + // Next try System32 directory + pathlen = GetSystemDirectoryW(path, pathsize); +@@ -131,7 +133,9 @@ exit: + return NULL; + module = LoadPackagedLibrary(name_w, 0); + #else +-#define LOAD_FLAGS (LOAD_LIBRARY_SEARCH_APPLICATION_DIR | LOAD_LIBRARY_SEARCH_SYSTEM32) ++// #define LOAD_FLAGS (LOAD_LIBRARY_SEARCH_APPLICATION_DIR | LOAD_LIBRARY_SEARCH_SYSTEM32) ++// Don't dynamic-link libraries from the application directory. ++ #define LOAD_FLAGS LOAD_LIBRARY_SEARCH_SYSTEM32 + /* filename may be be in CP_ACP */ + if (!name_w) + return LoadLibraryExA(name, NULL, LOAD_FLAGS); diff --git a/res/vcpkg/ffmpeg/portfile.cmake b/res/vcpkg/ffmpeg/portfile.cmake index 1d46535d5ddf..9d09c526423b 100644 --- a/res/vcpkg/ffmpeg/portfile.cmake +++ b/res/vcpkg/ffmpeg/portfile.cmake @@ -25,6 +25,7 @@ vcpkg_from_github( patch/0007-fix-linux-configure.patch patch/0008-remove-amf-loop-query.patch patch/0009-fix-nvenc-reconfigure-blur.patch + patch/0010.disable-loading-DLLs-from-app-dir.patch ) if(SOURCE_PATH MATCHES " ") diff --git a/src/core_main.rs b/src/core_main.rs index 23d7706d4738..5264d5bfd1e2 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -31,7 +31,10 @@ macro_rules! my_println{ pub fn core_main() -> Option> { crate::load_custom_client(); #[cfg(windows)] - crate::platform::windows::bootstrap(); + if !crate::platform::windows::bootstrap() { + // return None to terminate the process + return None; + } let mut args = Vec::new(); let mut flutter_args = Vec::new(); let mut i = 0; diff --git a/src/flutter.rs b/src/flutter.rs index fe0a77e39d36..255a00e0fd5e 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -19,6 +19,7 @@ use serde_json::json; use std::{ collections::{HashMap, HashSet}, ffi::CString, + io::{Error as IoError, ErrorKind as IoErrorKind}, os::raw::{c_char, c_int, c_void}, str::FromStr, sync::{ @@ -50,7 +51,7 @@ lazy_static::lazy_static! { #[cfg(target_os = "windows")] lazy_static::lazy_static! { - pub static ref TEXTURE_RGBA_RENDERER_PLUGIN: Result = Library::open("texture_rgba_renderer_plugin.dll"); + pub static ref TEXTURE_RGBA_RENDERER_PLUGIN: Result = load_plugin_in_app_path("texture_rgba_renderer_plugin.dll"); } #[cfg(target_os = "linux")] @@ -65,7 +66,37 @@ lazy_static::lazy_static! { #[cfg(target_os = "windows")] lazy_static::lazy_static! { - pub static ref TEXTURE_GPU_RENDERER_PLUGIN: Result = Library::open("flutter_gpu_texture_renderer_plugin.dll"); + pub static ref TEXTURE_GPU_RENDERER_PLUGIN: Result = load_plugin_in_app_path("flutter_gpu_texture_renderer_plugin.dll"); +} + +// Move this function into `src/platform/windows.rs` if there're more calls to load plugins. +// Load dll with full path. +#[cfg(target_os = "windows")] +fn load_plugin_in_app_path(dll_name: &str) -> Result { + match std::env::current_exe() { + Ok(exe_file) => { + if let Some(cur_dir) = exe_file.parent() { + let full_path = cur_dir.join(dll_name); + if !full_path.exists() { + Err(LibError::OpeningLibraryError(IoError::new( + IoErrorKind::NotFound, + format!("{} not found", dll_name), + ))) + } else { + Library::open(full_path) + } + } else { + Err(LibError::OpeningLibraryError(IoError::new( + IoErrorKind::Other, + format!( + "Invalid exe parent for {}", + exe_file.to_string_lossy().as_ref() + ), + ))) + } + } + Err(e) => Err(LibError::OpeningLibraryError(e)), + } } /// FFI for rustdesk core's main entry. @@ -2076,11 +2107,7 @@ pub mod sessions { } pub(super) mod async_tasks { - use hbb_common::{ - bail, - tokio::{self, select}, - ResultType, - }; + use hbb_common::{bail, tokio, ResultType}; use std::{ collections::HashMap, sync::{ diff --git a/src/platform/windows.rs b/src/platform/windows.rs index 92d02220ea4a..6c0136128a1d 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -18,12 +18,11 @@ use hbb_common::{ use std::{ collections::HashMap, ffi::{CString, OsString}, - fs, io, - io::prelude::*, + fs, + io::{self, prelude::*}, mem, os::windows::process::CommandExt, path::*, - process::{Command, Stdio}, ptr::null_mut, sync::{atomic::Ordering, Arc, Mutex}, time::{Duration, Instant}, @@ -32,11 +31,13 @@ use wallpaper; use winapi::{ ctypes::c_void, shared::{minwindef::*, ntdef::NULL, windef::*, winerror::*}, - um::sysinfoapi::{GetNativeSystemInfo, SYSTEM_INFO}, um::{ errhandlingapi::GetLastError, handleapi::CloseHandle, - libloaderapi::{GetProcAddress, LoadLibraryA}, + libloaderapi::{ + GetProcAddress, LoadLibraryExA, LoadLibraryExW, LOAD_LIBRARY_SEARCH_SYSTEM32, + LOAD_LIBRARY_SEARCH_USER_DIRS, + }, minwinbase::STILL_ACTIVE, processthreadsapi::{ GetCurrentProcess, GetCurrentProcessId, GetExitCodeProcess, OpenProcess, @@ -44,6 +45,7 @@ use winapi::{ }, securitybaseapi::GetTokenInformation, shellapi::ShellExecuteW, + sysinfoapi::{GetNativeSystemInfo, SYSTEM_INFO}, winbase::*, wingdi::*, winnt::{ @@ -1563,10 +1565,63 @@ pub fn is_win_10_or_greater() -> bool { unsafe { is_windows_10_or_greater() > 0 } } -pub fn bootstrap() { +pub fn bootstrap() -> bool { if let Ok(lic) = get_license_from_exe_name() { *config::EXE_RENDEZVOUS_SERVER.write().unwrap() = lic.host.clone(); } + + set_safe_load_dll() +} + +fn set_safe_load_dll() -> bool { + if !unsafe { set_default_dll_directories() } { + return false; + } + + // `SetDllDirectoryW` should never fail. + // https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-setdlldirectoryw + if unsafe { SetDllDirectoryW(wide_string("").as_ptr()) == FALSE } { + eprintln!("SetDllDirectoryW failed: {}", io::Error::last_os_error()); + return false; + } + + true +} + +// https://docs.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-setdefaultdlldirectories +unsafe fn set_default_dll_directories() -> bool { + let module = LoadLibraryExW( + wide_string("Kernel32.dll").as_ptr(), + 0 as _, + LOAD_LIBRARY_SEARCH_SYSTEM32, + ); + if module.is_null() { + return false; + } + + match CString::new("SetDefaultDllDirectories") { + Err(e) => { + eprintln!("CString::new failed: {}", e); + return false; + } + Ok(func_name) => { + let func = GetProcAddress(module, func_name.as_ptr()); + if func.is_null() { + eprintln!("GetProcAddress failed: {}", io::Error::last_os_error()); + return false; + } + type SetDefaultDllDirectories = unsafe extern "system" fn(DWORD) -> BOOL; + let func: SetDefaultDllDirectories = std::mem::transmute(func); + if func(LOAD_LIBRARY_SEARCH_SYSTEM32 | LOAD_LIBRARY_SEARCH_USER_DIRS) == FALSE { + eprintln!( + "SetDefaultDllDirectories failed: {}", + io::Error::last_os_error() + ); + return false; + } + } + } + true } pub fn create_shortcut(id: &str) -> ResultType<()> { @@ -2530,7 +2585,11 @@ pub fn try_kill_rustdesk_main_window_process() -> ResultType<()> { fn nt_terminate_process(process_id: DWORD) -> ResultType<()> { type NtTerminateProcess = unsafe extern "system" fn(HANDLE, DWORD) -> DWORD; unsafe { - let h_module = LoadLibraryA(CString::new("ntdll.dll")?.as_ptr()); + let h_module = LoadLibraryExA( + CString::new("ntdll.dll")?.as_ptr(), + std::ptr::null_mut(), + LOAD_LIBRARY_SEARCH_SYSTEM32, + ); if !h_module.is_null() { let f_nt_terminate_process: NtTerminateProcess = std::mem::transmute(GetProcAddress( h_module, From 55005f81293469a7eb3f8d2541ad3443581b7e75 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Mon, 27 Jan 2025 16:16:44 +0800 Subject: [PATCH 537/541] fix: win, file clipboard, try empty (#10609) Signed-off-by: fufesou --- libs/clipboard/src/lib.rs | 11 ++++++ libs/clipboard/src/platform/windows.rs | 10 ++++-- libs/clipboard/src/windows/wf_cliprdr.c | 46 ++++++++++++++++++++++--- libs/hbb_common | 2 +- src/clipboard_file.rs | 10 ++++++ 5 files changed, 72 insertions(+), 7 deletions(-) diff --git a/libs/clipboard/src/lib.rs b/libs/clipboard/src/lib.rs index 30055740ed8c..6bdd2293aa63 100644 --- a/libs/clipboard/src/lib.rs +++ b/libs/clipboard/src/lib.rs @@ -107,6 +107,7 @@ pub enum ClipboardFile { stream_id: i32, requested_data: Vec, }, + TryEmpty, } struct MsgChannel { @@ -226,6 +227,16 @@ fn send_data_to_channel(conn_id: i32, data: ClipboardFile) -> ResultType<()> { } } +#[cfg(target_os = "windows")] +pub fn send_data_exclude(conn_id: i32, data: ClipboardFile) { + use hbb_common::log; + for msg_channel in VEC_MSG_CHANNEL.read().unwrap().iter() { + if msg_channel.conn_id != conn_id { + allow_err!(msg_channel.sender.send(data.clone())); + } + } +} + #[cfg(feature = "unix-file-copy-paste")] #[inline] fn send_data_to_all(data: ClipboardFile) -> ResultType<()> { diff --git a/libs/clipboard/src/platform/windows.rs b/libs/clipboard/src/platform/windows.rs index 5d1aa086ddbf..3b931a4a6516 100644 --- a/libs/clipboard/src/platform/windows.rs +++ b/libs/clipboard/src/platform/windows.rs @@ -6,10 +6,10 @@ #![allow(deref_nullptr)] use crate::{ - allow_err, send_data, ClipboardFile, CliprdrError, CliprdrServiceContext, ResultType, + send_data, send_data_exclude, ClipboardFile, CliprdrError, CliprdrServiceContext, ResultType, ERR_CODE_INVALID_PARAMETER, ERR_CODE_SEND_MSG, ERR_CODE_SERVER_FUNCTION_NONE, VEC_MSG_CHANNEL, }; -use hbb_common::log; +use hbb_common::{allow_err, log}; use std::{ boxed::Box, ffi::{CStr, CString}, @@ -643,6 +643,7 @@ pub fn server_clip_file( conn_id, &format_list ); + send_data_exclude(conn_id as _, ClipboardFile::TryEmpty); ret = server_format_list(context, conn_id, format_list); log::debug!( "server_format_list called, conn_id {}, return {}", @@ -740,6 +741,11 @@ pub fn server_clip_file( ret ); } + ClipboardFile::TryEmpty => { + log::debug!("empty_clipboard called"); + let ret = empty_clipboard(context, conn_id); + log::debug!("empty_clipboard called, conn_id {}, return {}", conn_id, ret); + } } ret } diff --git a/libs/clipboard/src/windows/wf_cliprdr.c b/libs/clipboard/src/windows/wf_cliprdr.c index 6f8381a6ad40..e065be215c38 100644 --- a/libs/clipboard/src/windows/wf_cliprdr.c +++ b/libs/clipboard/src/windows/wf_cliprdr.c @@ -269,6 +269,7 @@ static UINT cliprdr_send_request_filecontents(wfClipboard *clipboard, UINT32 con DWORD positionlow, ULONG request); static BOOL is_file_descriptor_from_remote(); +static BOOL is_set_by_instance(wfClipboard *clipboard); static void CliprdrDataObject_Delete(CliprdrDataObject *instance); @@ -600,8 +601,11 @@ static CliprdrStream *CliprdrStream_New(UINT32 connID, ULONG index, void *pData, clipboard->req_fdata = NULL; } } - else + else { + instance->m_lSize.QuadPart = + ((UINT64)instance->m_Dsc.nFileSizeHigh << 32) | instance->m_Dsc.nFileSizeLow; success = TRUE; + } } } @@ -1745,8 +1749,7 @@ static LRESULT CALLBACK cliprdr_proc(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM DEBUG_CLIPRDR("info: WM_CLIPBOARDUPDATE"); // if (clipboard->sync) { - if ((GetClipboardOwner() != clipboard->hwnd) && - (S_FALSE == OleIsCurrentClipboard(clipboard->data_obj))) + if (!is_set_by_instance(clipboard)) { if (clipboard->hmem) { @@ -2086,6 +2089,8 @@ static FILEDESCRIPTORW *wf_cliprdr_get_file_descriptor(WCHAR *file_name, size_t return NULL; } + // to-do: use `fd->dwFlags = FD_ATTRIBUTES | FD_FILESIZE | FD_WRITESTIME | FD_PROGRESSUI`. + // We keep `fd->dwFlags = FD_ATTRIBUTES | FD_WRITESTIME | FD_PROGRESSUI` for compatibility. // fd->dwFlags = FD_ATTRIBUTES | FD_FILESIZE | FD_WRITESTIME | FD_PROGRESSUI; fd->dwFlags = FD_ATTRIBUTES | FD_WRITESTIME | FD_PROGRESSUI; fd->dwFileAttributes = GetFileAttributesW(file_name); @@ -2849,6 +2854,31 @@ wf_cliprdr_server_file_contents_request(CliprdrClientContext *context, goto exit; } + // If the clipboard is set by the instance, or the file descriptor is from remote, + // we should not process the request. + // Because this may be the following cases: + // 1. `A` -> `B`, `C` + // 2. Copy in `A` + // 3. Copy in `B` + // 4. Paste in `C` + // In this case, `C` should not get the file content from `A`. The clipboard is set by `B`. + // + // Or + // 1. `B` -> `A` -> `C` + // 2. Copy in `A` + // 2. Copy in `B` + // 3. Paste in `C` + // In this case, `C` should not get the file content from `A`. The clipboard is set by `B`. + // + // We can simply notify `C` to clear the clipboard when `A` received copy message from `B`, + // if connections are in the same process. + // But if connections are in different processes, it is not easy to notify the other process. + // So we just ignore the request from `C` in this case. + if (is_set_by_instance(clipboard) || is_file_descriptor_from_remote()) { + rc = ERROR_INTERNAL_ERROR; + goto exit; + } + cbRequested = fileContentsRequest->cbRequested; if (fileContentsRequest->dwFlags == FILECONTENTS_SIZE) cbRequested = sizeof(UINT64); @@ -3089,6 +3119,14 @@ wf_cliprdr_server_file_contents_response(CliprdrClientContext *context, return rc; } +BOOL is_set_by_instance(wfClipboard *clipboard) +{ + if (GetClipboardOwner() == clipboard->hwnd || S_OK == OleIsCurrentClipboard(clipboard->data_obj)) { + return TRUE; + } + return FALSE; +} + BOOL is_file_descriptor_from_remote() { UINT fsid = 0; @@ -3175,7 +3213,7 @@ BOOL wf_cliprdr_uninit(wfClipboard *clipboard, CliprdrClientContext *cliprdr) /* discard all contexts in clipboard */ if (try_open_clipboard(clipboard->hwnd)) { - if (is_file_descriptor_from_remote()) + if (is_set_by_instance(clipboard) || is_file_descriptor_from_remote()) { if (!EmptyClipboard()) { diff --git a/libs/hbb_common b/libs/hbb_common index 49c6b24a7a8c..79f8ac2d68e7 160000 --- a/libs/hbb_common +++ b/libs/hbb_common @@ -1 +1 @@ -Subproject commit 49c6b24a7a8c39d4448e07b743007ef1a3febd43 +Subproject commit 79f8ac2d68e7b3304773c553f91f1de825bacdf5 diff --git a/src/clipboard_file.rs b/src/clipboard_file.rs index a4bfc1aef699..4548cdbea1ae 100644 --- a/src/clipboard_file.rs +++ b/src/clipboard_file.rs @@ -134,6 +134,15 @@ pub fn clip_2_msg(clip: ClipboardFile) -> Message { })), ..Default::default() }, + ClipboardFile::TryEmpty => Message { + union: Some(message::Union::Cliprdr(Cliprdr { + union: Some(cliprdr::Union::TryEmpty(CliprdrTryEmpty { + ..Default::default() + })), + ..Default::default() + })), + ..Default::default() + }, } } @@ -176,6 +185,7 @@ pub fn msg_2_clip(msg: Cliprdr) -> Option { requested_data: data.requested_data.into(), }) } + Some(cliprdr::Union::TryEmpty(_)) => Some(ClipboardFile::TryEmpty), _ => None, } } From 25f917a7b456f18bff59d092a9c6cd9bb3b3e9ce Mon Sep 17 00:00:00 2001 From: RustDesk <71636191+rustdesk@users.noreply.github.com> Date: Tue, 28 Jan 2025 16:16:00 +0800 Subject: [PATCH 538/541] misused by bad guys (#10614) --- .github/workflows/flutter-build.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/flutter-build.yml b/.github/workflows/flutter-build.yml index bb43c39d3ecf..a795bfaeda05 100644 --- a/.github/workflows/flutter-build.yml +++ b/.github/workflows/flutter-build.yml @@ -38,10 +38,6 @@ env: #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 }}" From 5fc8e8c4284a780e56765b4ecc39f8df5883410c Mon Sep 17 00:00:00 2001 From: rustdesk Date: Wed, 29 Jan 2025 16:57:28 +0800 Subject: [PATCH 539/541] remove PUBLIC_RS_PUB_KE --- libs/hbb_common | 2 +- src/client.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/libs/hbb_common b/libs/hbb_common index 79f8ac2d68e7..97266d7c180f 160000 --- a/libs/hbb_common +++ b/libs/hbb_common @@ -1 +1 @@ -Subproject commit 79f8ac2d68e7b3304773c553f91f1de825bacdf5 +Subproject commit 97266d7c180feef8b43726ea6fcb4491e3fd8752 diff --git a/src/client.rs b/src/client.rs index 6833ad34f700..bdef9c7ed0fe 100644 --- a/src/client.rs +++ b/src/client.rs @@ -37,7 +37,7 @@ use hbb_common::{ bail, config::{ self, Config, LocalConfig, PeerConfig, PeerInfoSerde, Resolution, CONNECT_TIMEOUT, - PUBLIC_RS_PUB_KEY, READ_TIMEOUT, RELAY_PORT, RENDEZVOUS_PORT, RENDEZVOUS_SERVERS, + READ_TIMEOUT, RELAY_PORT, RENDEZVOUS_PORT, RENDEZVOUS_SERVERS, }, get_version_number, log, message_proto::{option_message::BoolOption, *}, @@ -1475,7 +1475,7 @@ impl LoginConfigHandler { let server = server_key.next().unwrap_or_default(); let args = server_key.next().unwrap_or_default(); let key = if server == PUBLIC_SERVER { - PUBLIC_RS_PUB_KEY.to_owned() + config::RS_PUB_KEY.to_owned() } else { let mut args_map: HashMap = HashMap::new(); for arg in args.split('&') { From 8b24b195a2348fc0bec050a63cc271077973b034 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Thu, 30 Jan 2025 13:49:37 +0800 Subject: [PATCH 540/541] remove useless files --- entrypoint.sh | 36 - vdi/README.md | 1 - vdi/host/.cargo/config.toml | 2 - vdi/host/.gitignore | 1 - vdi/host/Cargo.lock | 2543 ----------------------------------- vdi/host/Cargo.toml | 13 - vdi/host/README.md | 5 - vdi/host/src/connection.rs | 11 - vdi/host/src/console.rs | 119 -- vdi/host/src/lib.rs | 3 - vdi/host/src/main.rs | 6 - vdi/host/src/server.rs | 172 --- 12 files changed, 2912 deletions(-) delete mode 100755 entrypoint.sh delete mode 100644 vdi/README.md delete mode 100644 vdi/host/.cargo/config.toml delete mode 100644 vdi/host/.gitignore delete mode 100644 vdi/host/Cargo.lock delete mode 100644 vdi/host/Cargo.toml delete mode 100644 vdi/host/README.md delete mode 100644 vdi/host/src/connection.rs delete mode 100644 vdi/host/src/console.rs delete mode 100644 vdi/host/src/lib.rs delete mode 100644 vdi/host/src/main.rs delete mode 100644 vdi/host/src/server.rs diff --git a/entrypoint.sh b/entrypoint.sh deleted file mode 100755 index 8c7be0786ed5..000000000000 --- a/entrypoint.sh +++ /dev/null @@ -1,36 +0,0 @@ -#!/bin/sh - -cd "$HOME"/rustdesk || exit 1 -# shellcheck source=/dev/null -. "$HOME"/.cargo/env - -argv=$* - -while test $# -gt 0; do - case "$1" in - --release) - mkdir -p target/release - test -f target/release/libsciter-gtk.so || cp "$HOME"/libsciter-gtk.so target/release/ - release=1 - shift - ;; - --target) - shift - if test $# -gt 0; then - rustup target add "$1" - shift - fi - ;; - *) - shift - ;; - esac -done - -if [ -z $release ]; then - mkdir -p target/debug - test -f target/debug/libsciter-gtk.so || cp "$HOME"/libsciter-gtk.so target/debug/ -fi -set -f -#shellcheck disable=2086 -VCPKG_ROOT=/vcpkg cargo build $argv diff --git a/vdi/README.md b/vdi/README.md deleted file mode 100644 index 85e6ff194b98..000000000000 --- a/vdi/README.md +++ /dev/null @@ -1 +0,0 @@ -# WIP diff --git a/vdi/host/.cargo/config.toml b/vdi/host/.cargo/config.toml deleted file mode 100644 index 70f9eaeb2704..000000000000 --- a/vdi/host/.cargo/config.toml +++ /dev/null @@ -1,2 +0,0 @@ -[registries.crates-io] -protocol = "sparse" diff --git a/vdi/host/.gitignore b/vdi/host/.gitignore deleted file mode 100644 index ea8c4bf7f35f..000000000000 --- a/vdi/host/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/target diff --git a/vdi/host/Cargo.lock b/vdi/host/Cargo.lock deleted file mode 100644 index 0b2e8ca2b8d3..000000000000 --- a/vdi/host/Cargo.lock +++ /dev/null @@ -1,2543 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 3 - -[[package]] -name = "addr2line" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a76fd60b23679b7d19bd066031410fb7e458ccc5e958eb5c325888ce4baedc97" -dependencies = [ - "gimli", -] - -[[package]] -name = "adler" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" - -[[package]] -name = "ahash" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" -dependencies = [ - "getrandom", - "once_cell", - "version_check", -] - -[[package]] -name = "aho-corasick" -version = "0.7.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" -dependencies = [ - "memchr", -] - -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - -[[package]] -name = "anyhow" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "224afbd727c3d6e4b90103ece64b8d1b67fbb1973b1046c2281eed3f3803f800" - -[[package]] -name = "async-broadcast" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90622698a1218e0b2fb846c97b5f19a0831f6baddee73d9454156365ccfa473b" -dependencies = [ - "easy-parallel", - "event-listener", - "futures-core", -] - -[[package]] -name = "async-broadcast" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d26004fe83b2d1cd3a97609b21e39f9a31535822210fe83205d2ce48866ea61" -dependencies = [ - "event-listener", - "futures-core", - "parking_lot", -] - -[[package]] -name = "async-channel" -version = "1.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf46fee83e5ccffc220104713af3292ff9bc7c64c7de289f66dae8e38d826833" -dependencies = [ - "concurrent-queue", - "event-listener", - "futures-core", -] - -[[package]] -name = "async-executor" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17adb73da160dfb475c183343c8cccd80721ea5a605d3eb57125f0a7b7a92d0b" -dependencies = [ - "async-lock", - "async-task", - "concurrent-queue", - "fastrand", - "futures-lite", - "slab", -] - -[[package]] -name = "async-io" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c374dda1ed3e7d8f0d9ba58715f924862c63eae6849c92d3a18e7fbde9e2794" -dependencies = [ - "async-lock", - "autocfg", - "concurrent-queue", - "futures-lite", - "libc", - "log", - "parking", - "polling", - "slab", - "socket2 0.4.7", - "waker-fn", - "windows-sys 0.42.0", -] - -[[package]] -name = "async-lock" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8101efe8695a6c17e02911402145357e718ac92d3ff88ae8419e84b1707b685" -dependencies = [ - "event-listener", - "futures-lite", -] - -[[package]] -name = "async-recursion" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7d78656ba01f1b93024b7c3a0467f1608e4be67d725749fdcd7d2c7678fd7a2" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "async-task" -version = "4.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a40729d2133846d9ed0ea60a8b9541bccddab49cd30f0715a1da672fe9a2524" - -[[package]] -name = "async-trait" -version = "0.1.64" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cd7fce9ba8c3c042128ce72d8b2ddbf3a05747efb67ea0313c635e10bda47a2" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "autocfg" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" - -[[package]] -name = "backtrace" -version = "0.3.67" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "233d376d6d185f2a3093e58f283f60f880315b6c60075b01f36b3b85154564ca" -dependencies = [ - "addr2line", - "cc", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", -] - -[[package]] -name = "bit_field" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61" - -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "bitflags" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "487f1e0fcbe47deb8b0574e646def1c903389d95241dd1bbcc6ce4a715dfc0c1" - -[[package]] -name = "bumpalo" -version = "3.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535" - -[[package]] -name = "bytemuck" -version = "1.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17febce684fd15d89027105661fec94afb475cb995fbc59d2865198446ba2eea" - -[[package]] -name = "byteorder" -version = "1.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" - -[[package]] -name = "bytes" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" -dependencies = [ - "serde", -] - -[[package]] -name = "cc" -version = "1.0.79" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" -dependencies = [ - "jobserver", -] - -[[package]] -name = "cfg-if" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" - -[[package]] -name = "chrono" -version = "0.4.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16b0a3d9ed01224b22057780a37bb8c5dbfe1be8ba48678e7bf57ec4b385411f" -dependencies = [ - "iana-time-zone", - "js-sys", - "num-integer", - "num-traits", - "time", - "wasm-bindgen", - "winapi", -] - -[[package]] -name = "clap" -version = "4.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42dfd32784433290c51d92c438bb72ea5063797fc3cc9a21a8c4346bebbb2098" -dependencies = [ - "bitflags 2.0.2", - "clap_derive", - "clap_lex", - "is-terminal", - "once_cell", - "strsim", - "termcolor", -] - -[[package]] -name = "clap_derive" -version = "4.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fddf67631444a3a3e3e5ac51c36a5e01335302de677bd78759eaa90ab1f46644" -dependencies = [ - "heck", - "proc-macro-error", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "clap_lex" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "033f6b7a4acb1f358c742aaca805c939ee73b4c6209ae4318ec7aca81c42e646" -dependencies = [ - "os_str_bytes", -] - -[[package]] -name = "codespan-reporting" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" -dependencies = [ - "termcolor", - "unicode-width", -] - -[[package]] -name = "color_quant" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" - -[[package]] -name = "concurrent-queue" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c278839b831783b70278b14df4d45e1beb1aad306c07bb796637de9a0e323e8e" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "confy" -version = "0.4.0" -source = "git+https://github.com/open-trade/confy#630cc28a396cb7d01eefdd9f3824486fe4d8554b" -dependencies = [ - "directories-next", - "serde", - "thiserror", - "toml", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" - -[[package]] -name = "crc32fast" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "crossbeam-channel" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2dd04ddaf88237dc3b8d8f9a3c1004b506b54b3313403944054d23c0870c521" -dependencies = [ - "cfg-if", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-deque" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "715e8152b692bba2d374b53d4875445368fdf21a94751410af607a5ac677d1fc" -dependencies = [ - "cfg-if", - "crossbeam-epoch", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-epoch" -version = "0.9.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01a9af1f4c2ef74bb8aa1f7e19706bc72d03598c8a570bb5de72243c7a9d9d5a" -dependencies = [ - "autocfg", - "cfg-if", - "crossbeam-utils", - "memoffset 0.7.1", - "scopeguard", -] - -[[package]] -name = "crossbeam-queue" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1cfb3ea8a53f37c40dea2c7bedcbd88bdfae54f5e2175d6ecaff1c988353add" -dependencies = [ - "cfg-if", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-utils" -version = "0.8.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fb766fa798726286dbbb842f174001dab8abc7b627a1dd86e0b7222a95d929f" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "crunchy" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" - -[[package]] -name = "cxx" -version = "1.0.91" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86d3488e7665a7a483b57e25bdd90d0aeb2bc7608c8d0346acf2ad3f1caf1d62" -dependencies = [ - "cc", - "cxxbridge-flags", - "cxxbridge-macro", - "link-cplusplus", -] - -[[package]] -name = "cxx-build" -version = "1.0.91" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48fcaf066a053a41a81dfb14d57d99738b767febb8b735c3016e469fac5da690" -dependencies = [ - "cc", - "codespan-reporting", - "once_cell", - "proc-macro2", - "quote", - "scratch", - "syn", -] - -[[package]] -name = "cxxbridge-flags" -version = "1.0.91" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2ef98b8b717a829ca5603af80e1f9e2e48013ab227b68ef37872ef84ee479bf" - -[[package]] -name = "cxxbridge-macro" -version = "1.0.91" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "086c685979a698443656e5cf7856c95c642295a38599f12fb1ff76fb28d19892" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "derivative" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "directories-next" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "339ee130d97a610ea5a5872d2bbb130fdf68884ff09d3028b81bec8a1ac23bbc" -dependencies = [ - "cfg-if", - "dirs-sys-next", -] - -[[package]] -name = "dirs" -version = "4.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" -dependencies = [ - "dirs-sys", -] - -[[package]] -name = "dirs-next" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" -dependencies = [ - "cfg-if", - "dirs-sys-next", -] - -[[package]] -name = "dirs-sys" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" -dependencies = [ - "libc", - "redox_users", - "winapi", -] - -[[package]] -name = "dirs-sys-next" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" -dependencies = [ - "libc", - "redox_users", - "winapi", -] - -[[package]] -name = "easy-parallel" -version = "3.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6907e25393cdcc1f4f3f513d9aac1e840eb1cc341a0fccb01171f7d14d10b946" - -[[package]] -name = "ed25519" -version = "1.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91cff35c70bba8a626e3185d8cd48cc11b5437e1a5bcd15b9b5fa3c64b6dfee7" -dependencies = [ - "signature", -] - -[[package]] -name = "either" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" - -[[package]] -name = "enumflags2" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e75d4cd21b95383444831539909fbb14b9dc3fdceb2a6f5d36577329a1f55ccb" -dependencies = [ - "enumflags2_derive", - "serde", -] - -[[package]] -name = "enumflags2_derive" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f58dc3c5e468259f19f2d46304a6b28f1c3d034442e14b322d2b850e36f6d5ae" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "env_logger" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85cdab6a89accf66733ad5a1693a4dcced6aeff64602b634530dd73c1f3ee9f0" -dependencies = [ - "humantime", - "is-terminal", - "log", - "regex", - "termcolor", -] - -[[package]] -name = "errno" -version = "0.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" -dependencies = [ - "errno-dragonfly", - "libc", - "winapi", -] - -[[package]] -name = "errno-dragonfly" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" -dependencies = [ - "cc", - "libc", -] - -[[package]] -name = "event-listener" -version = "2.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" - -[[package]] -name = "exr" -version = "1.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdd2162b720141a91a054640662d3edce3d50a944a50ffca5313cd951abb35b4" -dependencies = [ - "bit_field", - "flume", - "half", - "lebe", - "miniz_oxide", - "rayon-core", - "smallvec", - "zune-inflate", -] - -[[package]] -name = "fastrand" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" -dependencies = [ - "instant", -] - -[[package]] -name = "filetime" -version = "0.2.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a3de6e8d11b22ff9edc6d916f890800597d60f8b2da1caf2955c274638d6412" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "windows-sys 0.45.0", -] - -[[package]] -name = "flate2" -version = "1.0.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8a2db397cb1c8772f31494cb8917e48cd1e64f0fa7efac59fbd741a0a8ce841" -dependencies = [ - "crc32fast", - "miniz_oxide", -] - -[[package]] -name = "flexi_logger" -version = "0.25.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6eae57842a8221ef13f1f207632d786a175dd13bd8fbdc8be9d852f7c9cf1046" -dependencies = [ - "chrono", - "crossbeam-channel", - "crossbeam-queue", - "glob", - "is-terminal", - "lazy_static", - "log", - "nu-ansi-term", - "regex", - "thiserror", -] - -[[package]] -name = "flume" -version = "0.10.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1657b4441c3403d9f7b3409e47575237dac27b1b5726df654a6ecbf92f0f7577" -dependencies = [ - "futures-core", - "futures-sink", - "nanorand", - "pin-project", - "spin", -] - -[[package]] -name = "futures" -version = "0.3.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13e2792b0ff0340399d58445b88fd9770e3489eff258a4cbc1523418f12abf84" -dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-channel" -version = "0.3.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e5317663a9089767a1ec00a487df42e0ca174b61b4483213ac24448e4664df5" -dependencies = [ - "futures-core", - "futures-sink", -] - -[[package]] -name = "futures-core" -version = "0.3.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec90ff4d0fe1f57d600049061dc6bb68ed03c7d2fbd697274c41805dcb3f8608" - -[[package]] -name = "futures-executor" -version = "0.3.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8de0a35a6ab97ec8869e32a2473f4b1324459e14c29275d14b10cb1fd19b50e" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-io" -version = "0.3.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfb8371b6fb2aeb2d280374607aeabfc99d95c72edfe51692e42d3d7f0d08531" - -[[package]] -name = "futures-lite" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7694489acd39452c77daa48516b894c153f192c3578d5a839b62c58099fcbf48" -dependencies = [ - "fastrand", - "futures-core", - "futures-io", - "memchr", - "parking", - "pin-project-lite", - "waker-fn", -] - -[[package]] -name = "futures-macro" -version = "0.3.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95a73af87da33b5acf53acfebdc339fe592ecf5357ac7c0a7734ab9d8c876a70" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "futures-sink" -version = "0.3.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f310820bb3e8cfd46c80db4d7fb8353e15dfff853a127158425f31e0be6c8364" - -[[package]] -name = "futures-task" -version = "0.3.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcf79a1bf610b10f42aea489289c5a2c478a786509693b80cd39c44ccd936366" - -[[package]] -name = "futures-util" -version = "0.3.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c1d6de3acfef38d2be4b1f543f553131788603495be83da675e180c8d6b7bd1" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "pin-utils", - "slab", -] - -[[package]] -name = "getrandom" -version = "0.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" -dependencies = [ - "cfg-if", - "js-sys", - "libc", - "wasi 0.11.0+wasi-snapshot-preview1", - "wasm-bindgen", -] - -[[package]] -name = "gif" -version = "0.11.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3edd93c6756b4dfaf2709eafcc345ba2636565295c198a9cfbf75fa5e3e00b06" -dependencies = [ - "color_quant", - "weezl", -] - -[[package]] -name = "gimli" -version = "0.27.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad0a93d233ebf96623465aad4046a8d3aa4da22d4f4beba5388838c8a434bbb4" - -[[package]] -name = "glob" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" - -[[package]] -name = "half" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02b4af3693f1b705df946e9fe5631932443781d0aabb423b62fcd4d73f6d2fd0" -dependencies = [ - "crunchy", -] - -[[package]] -name = "hashbrown" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" -dependencies = [ - "ahash", -] - -[[package]] -name = "hbb_common" -version = "0.1.0" -dependencies = [ - "anyhow", - "backtrace", - "bytes", - "chrono", - "confy", - "directories-next", - "dirs-next", - "env_logger", - "filetime", - "flexi_logger", - "futures", - "futures-util", - "lazy_static", - "libc", - "log", - "mac_address", - "machine-uid", - "osascript", - "protobuf", - "protobuf-codegen", - "rand", - "regex", - "serde", - "serde_derive", - "socket2 0.3.19", - "sodiumoxide", - "sysinfo", - "tokio", - "tokio-socks", - "tokio-util", - "winapi", - "zstd", -] - -[[package]] -name = "heck" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" - -[[package]] -name = "hermit-abi" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" -dependencies = [ - "libc", -] - -[[package]] -name = "hermit-abi" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" - -[[package]] -name = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" - -[[package]] -name = "humantime" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" - -[[package]] -name = "iana-time-zone" -version = "0.1.53" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64c122667b287044802d6ce17ee2ddf13207ed924c712de9a66a5814d5b64765" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "wasm-bindgen", - "winapi", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca" -dependencies = [ - "cxx", - "cxx-build", -] - -[[package]] -name = "image" -version = "0.24.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69b7ea949b537b0fd0af141fff8c77690f2ce96f4f41f042ccb6c69c6c965945" -dependencies = [ - "bytemuck", - "byteorder", - "color_quant", - "exr", - "gif", - "jpeg-decoder", - "num-rational", - "num-traits", - "png", - "scoped_threadpool", - "tiff", -] - -[[package]] -name = "indexmap" -version = "1.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399" -dependencies = [ - "autocfg", - "hashbrown", -] - -[[package]] -name = "instant" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "io-lifetimes" -version = "1.0.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09270fd4fa1111bc614ed2246c7ef56239a3063d5be0d1ec3b589c505d400aeb" -dependencies = [ - "hermit-abi 0.3.1", - "libc", - "windows-sys 0.45.0", -] - -[[package]] -name = "is-terminal" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8687c819457e979cc940d09cb16e42a1bf70aa6b60a549de6d3a62a0ee90c69e" -dependencies = [ - "hermit-abi 0.3.1", - "io-lifetimes", - "rustix", - "windows-sys 0.45.0", -] - -[[package]] -name = "itoa" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fad582f4b9e86b6caa621cabeb0963332d92eea04729ab12892c2533951e6440" - -[[package]] -name = "jobserver" -version = "0.1.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "068b1ee6743e4d11fb9c6a1e6064b3693a1b600e7f5f5988047d98b3dc9fb90b" -dependencies = [ - "libc", -] - -[[package]] -name = "jpeg-decoder" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc0000e42512c92e31c2252315bda326620a4e034105e900c98ec492fa077b3e" -dependencies = [ - "rayon", -] - -[[package]] -name = "js-sys" -version = "0.3.61" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "445dde2150c55e483f3d8416706b97ec8e8237c307e5b7b4b8dd15e6af2a0730" -dependencies = [ - "wasm-bindgen", -] - -[[package]] -name = "lazy_static" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" - -[[package]] -name = "lebe" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" - -[[package]] -name = "libc" -version = "0.2.139" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" - -[[package]] -name = "libsodium-sys" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b779387cd56adfbc02ea4a668e704f729be8d6a6abd2c27ca5ee537849a92fd" -dependencies = [ - "cc", - "libc", - "pkg-config", - "walkdir", -] - -[[package]] -name = "libusb1-sys" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9d0e2afce4245f2c9a418511e5af8718bcaf2fa408aefb259504d1a9cb25f27" -dependencies = [ - "cc", - "libc", - "pkg-config", - "vcpkg", -] - -[[package]] -name = "link-cplusplus" -version = "1.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecd207c9c713c34f95a097a5b029ac2ce6010530c7b49d7fea24d977dede04f5" -dependencies = [ - "cc", -] - -[[package]] -name = "linux-raw-sys" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4" - -[[package]] -name = "lock_api" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" -dependencies = [ - "autocfg", - "scopeguard", -] - -[[package]] -name = "log" -version = "0.4.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "mac_address" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b238e3235c8382b7653c6408ed1b08dd379bdb9fdf990fb0bbae3db2cc0ae963" -dependencies = [ - "nix 0.23.2", - "winapi", -] - -[[package]] -name = "machine-uid" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f1595709b0a7386bcd56ba34d250d626e5503917d05d32cdccddcd68603e212" -dependencies = [ - "winreg", -] - -[[package]] -name = "memchr" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" - -[[package]] -name = "memoffset" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" -dependencies = [ - "autocfg", -] - -[[package]] -name = "memoffset" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" -dependencies = [ - "autocfg", -] - -[[package]] -name = "miniz_oxide" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa" -dependencies = [ - "adler", -] - -[[package]] -name = "mio" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9" -dependencies = [ - "libc", - "log", - "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.45.0", -] - -[[package]] -name = "nanorand" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" -dependencies = [ - "getrandom", -] - -[[package]] -name = "nix" -version = "0.23.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f3790c00a0150112de0f4cd161e3d7fc4b2d8a5542ffc35f099a2562aecb35c" -dependencies = [ - "bitflags 1.3.2", - "cc", - "cfg-if", - "libc", - "memoffset 0.6.5", -] - -[[package]] -name = "nix" -version = "0.24.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa52e972a9a719cecb6864fb88568781eb706bac2cd1d4f04a648542dbf78069" -dependencies = [ - "bitflags 1.3.2", - "cfg-if", - "libc", - "memoffset 0.6.5", -] - -[[package]] -name = "nom8" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae01545c9c7fc4486ab7debaf2aad7003ac19431791868fb2e8066df97fad2f8" -dependencies = [ - "memchr", -] - -[[package]] -name = "ntapi" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc51db7b362b205941f71232e56c625156eb9a929f8cf74a428fd5bc094a4afc" -dependencies = [ - "winapi", -] - -[[package]] -name = "nu-ansi-term" -version = "0.46.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" -dependencies = [ - "overload", - "winapi", -] - -[[package]] -name = "num-integer" -version = "0.1.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" -dependencies = [ - "autocfg", - "num-traits", -] - -[[package]] -name = "num-rational" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" -dependencies = [ - "autocfg", - "num-integer", - "num-traits", -] - -[[package]] -name = "num-traits" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" -dependencies = [ - "autocfg", -] - -[[package]] -name = "num_cpus" -version = "1.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" -dependencies = [ - "hermit-abi 0.2.6", - "libc", -] - -[[package]] -name = "object" -version = "0.30.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea86265d3d3dcb6a27fc51bd29a4bf387fae9d2986b823079d4986af253eb439" -dependencies = [ - "memchr", -] - -[[package]] -name = "once_cell" -version = "1.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" - -[[package]] -name = "ordered-stream" -version = "0.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44630c059eacfd6e08bdaa51b1db2ce33119caa4ddc1235e923109aa5f25ccb1" -dependencies = [ - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "os_str_bytes" -version = "6.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ceedf44fb00f2d1984b0bc98102627ce622e083e49a5bacdb3e514fa4238e267" - -[[package]] -name = "osascript" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38731fa859ef679f1aec66ca9562165926b442f298467f76f5990f431efe87dc" -dependencies = [ - "serde", - "serde_derive", - "serde_json", -] - -[[package]] -name = "overload" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" - -[[package]] -name = "parking" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "427c3892f9e783d91cc128285287e70a59e206ca452770ece88a76f7a3eddd72" - -[[package]] -name = "parking_lot" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "smallvec", - "windows-sys 0.45.0", -] - -[[package]] -name = "pin-project" -version = "1.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad29a609b6bcd67fee905812e544992d216af9d755757c05ed2d0e15a74c6ecc" -dependencies = [ - "pin-project-internal", -] - -[[package]] -name = "pin-project-internal" -version = "1.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "069bdb1e05adc7a8990dce9cc75370895fbe4e3d58b9b73bf1aee56359344a55" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "pin-project-lite" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" - -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - -[[package]] -name = "pkg-config" -version = "0.3.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" - -[[package]] -name = "png" -version = "0.17.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d708eaf860a19b19ce538740d2b4bdeeb8337fa53f7738455e706623ad5c638" -dependencies = [ - "bitflags 1.3.2", - "crc32fast", - "flate2", - "miniz_oxide", -] - -[[package]] -name = "polling" -version = "2.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22122d5ec4f9fe1b3916419b76be1e80bcb93f618d071d2edf841b137b2a2bd6" -dependencies = [ - "autocfg", - "cfg-if", - "libc", - "log", - "wepoll-ffi", - "windows-sys 0.42.0", -] - -[[package]] -name = "ppv-lite86" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" - -[[package]] -name = "proc-macro-crate" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66618389e4ec1c7afe67d51a9bf34ff9236480f8d51e7489b7d5ab0303c13f34" -dependencies = [ - "once_cell", - "toml_edit", -] - -[[package]] -name = "proc-macro-error" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" -dependencies = [ - "proc-macro-error-attr", - "proc-macro2", - "quote", - "syn", - "version_check", -] - -[[package]] -name = "proc-macro-error-attr" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" -dependencies = [ - "proc-macro2", - "quote", - "version_check", -] - -[[package]] -name = "proc-macro2" -version = "1.0.51" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d727cae5b39d21da60fa540906919ad737832fe0b1c165da3a34d6548c849d6" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "protobuf" -version = "3.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b55bad9126f378a853655831eb7363b7b01b81d19f8cb1218861086ca4a1a61e" -dependencies = [ - "bytes", - "once_cell", - "protobuf-support", - "thiserror", -] - -[[package]] -name = "protobuf-codegen" -version = "3.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dd418ac3c91caa4032d37cb80ff0d44e2ebe637b2fb243b6234bf89cdac4901" -dependencies = [ - "anyhow", - "once_cell", - "protobuf", - "protobuf-parse", - "regex", - "tempfile", - "thiserror", -] - -[[package]] -name = "protobuf-parse" -version = "3.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d39b14605eaa1f6a340aec7f320b34064feb26c93aec35d6a9a2272a8ddfa49" -dependencies = [ - "anyhow", - "indexmap", - "log", - "protobuf", - "protobuf-support", - "tempfile", - "thiserror", - "which", -] - -[[package]] -name = "protobuf-support" -version = "3.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5d4d7b8601c814cfb36bcebb79f0e61e45e1e93640cf778837833bbed05c372" -dependencies = [ - "thiserror", -] - -[[package]] -name = "qemu-display" -version = "0.1.0" -source = "git+https://github.com/rustdesk/qemu-display#e8a0925c2e804aa1eb07ee3027deaf8dd1c71b1d" -dependencies = [ - "async-broadcast 0.3.4", - "async-lock", - "async-trait", - "cfg-if", - "derivative", - "enumflags2", - "futures", - "futures-util", - "libc", - "log", - "once_cell", - "serde", - "serde_bytes", - "serde_repr", - "uds_windows", - "usbredirhost", - "windows", - "zbus", - "zvariant", -] - -[[package]] -name = "qemu-rustdesk" -version = "0.1.0" -dependencies = [ - "async-trait", - "clap", - "hbb_common", - "image", - "qemu-display", - "zbus", -] - -[[package]] -name = "quote" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "libc", - "rand_chacha", - "rand_core", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom", -] - -[[package]] -name = "rayon" -version = "1.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db3a213adf02b3bcfd2d3846bb41cb22857d131789e01df434fb7e7bc0759b7" -dependencies = [ - "either", - "rayon-core", -] - -[[package]] -name = "rayon-core" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b8f95bd6966f5c87776639160a66bd8ab9895d9d4ab01ddba9fc60661aebe8d" -dependencies = [ - "crossbeam-channel", - "crossbeam-deque", - "crossbeam-utils", - "num_cpus", -] - -[[package]] -name = "redox_syscall" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" -dependencies = [ - "bitflags 1.3.2", -] - -[[package]] -name = "redox_users" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" -dependencies = [ - "getrandom", - "redox_syscall", - "thiserror", -] - -[[package]] -name = "regex" -version = "1.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48aaa5748ba571fb95cd2c85c09f629215d3a6ece942baa100950af03a34f733" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.6.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" - -[[package]] -name = "remove_dir_all" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" -dependencies = [ - "winapi", -] - -[[package]] -name = "rusb" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "703aa035c21c589b34fb5136b12e68fc8dcf7ea46486861381361dd8ebf5cee0" -dependencies = [ - "libc", - "libusb1-sys", -] - -[[package]] -name = "rustc-demangle" -version = "0.1.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342" - -[[package]] -name = "rustix" -version = "0.36.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db4165c9963ab29e422d6c26fbc1d37f15bace6b2810221f9d925023480fcf0e" -dependencies = [ - "bitflags 1.3.2", - "errno", - "io-lifetimes", - "libc", - "linux-raw-sys", - "windows-sys 0.45.0", -] - -[[package]] -name = "ryu" -version = "1.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b4b9743ed687d4b4bcedf9ff5eaa7398495ae14e61cba0a295704edbc7decde" - -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "scoped_threadpool" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d51f5df5af43ab3f1360b429fa5e0152ac5ce8c0bd6485cae490332e96846a8" - -[[package]] -name = "scopeguard" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" - -[[package]] -name = "scratch" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddccb15bcce173023b3fedd9436f882a0739b8dfb45e4f6b6002bee5929f61b2" - -[[package]] -name = "serde" -version = "1.0.152" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde-xml-rs" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0bf1ba0696ccf0872866277143ff1fd14d22eec235d2b23702f95e6660f7dfa" -dependencies = [ - "log", - "serde", - "thiserror", - "xml-rs", -] - -[[package]] -name = "serde_bytes" -version = "0.11.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "416bda436f9aab92e02c8e10d49a15ddd339cea90b6e340fe51ed97abb548294" -dependencies = [ - "serde", -] - -[[package]] -name = "serde_derive" -version = "1.0.152" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "serde_json" -version = "1.0.93" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cad406b69c91885b5107daf2c29572f6c8cdb3c66826821e286c533490c0bc76" -dependencies = [ - "itoa", - "ryu", - "serde", -] - -[[package]] -name = "serde_repr" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a5ec9fa74a20ebbe5d9ac23dac1fc96ba0ecfe9f50f2843b52e537b10fbcb4e" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "sha1" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1da05c97445caa12d05e848c4a4fcbbea29e748ac28f7e80e9b010392063770" -dependencies = [ - "sha1_smol", -] - -[[package]] -name = "sha1_smol" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012" - -[[package]] -name = "signal-hook-registry" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" -dependencies = [ - "libc", -] - -[[package]] -name = "signature" -version = "1.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" - -[[package]] -name = "simd-adler32" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "238abfbb77c1915110ad968465608b68e869e0772622c9656714e73e5a1a522f" - -[[package]] -name = "slab" -version = "0.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" -dependencies = [ - "autocfg", -] - -[[package]] -name = "smallvec" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" - -[[package]] -name = "socket2" -version = "0.3.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "122e570113d28d773067fab24266b66753f6ea915758651696b6e35e49f88d6e" -dependencies = [ - "cfg-if", - "libc", - "winapi", -] - -[[package]] -name = "socket2" -version = "0.4.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02e2d2db9033d13a1567121ddd7a095ee144db4e1ca1b1bda3419bc0da294ebd" -dependencies = [ - "libc", - "winapi", -] - -[[package]] -name = "sodiumoxide" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e26be3acb6c2d9a7aac28482586a7856436af4cfe7100031d219de2d2ecb0028" -dependencies = [ - "ed25519", - "libc", - "libsodium-sys", - "serde", -] - -[[package]] -name = "spin" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5d6e0250b93c8427a177b849d144a96d5acc57006149479403d7861ab721e34" -dependencies = [ - "lock_api", -] - -[[package]] -name = "static_assertions" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" - -[[package]] -name = "strsim" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" - -[[package]] -name = "syn" -version = "1.0.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "sysinfo" -version = "0.28.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f69e0d827cce279e61c2f3399eb789271a8f136d8245edef70f06e3c9601a670" -dependencies = [ - "cfg-if", - "core-foundation-sys", - "libc", - "ntapi", - "once_cell", - "rayon", - "winapi", -] - -[[package]] -name = "tempfile" -version = "3.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" -dependencies = [ - "cfg-if", - "fastrand", - "libc", - "redox_syscall", - "remove_dir_all", - "winapi", -] - -[[package]] -name = "termcolor" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "thiserror" -version = "1.0.38" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a9cd18aa97d5c45c6603caea1da6628790b37f7a34b6ca89522331c5180fed0" -dependencies = [ - "thiserror-impl", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.38" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "tiff" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7449334f9ff2baf290d55d73983a7d6fa15e01198faef72af07e2a8db851e471" -dependencies = [ - "flate2", - "jpeg-decoder", - "weezl", -] - -[[package]] -name = "time" -version = "0.1.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" -dependencies = [ - "libc", - "wasi 0.10.0+wasi-snapshot-preview1", - "winapi", -] - -[[package]] -name = "tokio" -version = "1.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8e00990ebabbe4c14c08aca901caed183ecd5c09562a12c824bb53d3c3fd3af" -dependencies = [ - "autocfg", - "bytes", - "libc", - "memchr", - "mio", - "num_cpus", - "parking_lot", - "pin-project-lite", - "signal-hook-registry", - "socket2 0.4.7", - "tokio-macros", - "windows-sys 0.42.0", -] - -[[package]] -name = "tokio-macros" -version = "1.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d266c00fde287f55d3f1c3e96c500c362a2b8c695076ec180f27918820bc6df8" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "tokio-socks" -version = "0.5.1-1" -source = "git+https://github.com/open-trade/tokio-socks#7034e79263ce25c348be072808d7601d82cd892d" -dependencies = [ - "bytes", - "either", - "futures-core", - "futures-sink", - "futures-util", - "pin-project", - "thiserror", - "tokio", - "tokio-util", -] - -[[package]] -name = "tokio-util" -version = "0.7.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5427d89453009325de0d8f342c9490009f76e999cb7672d77e46267448f7e6b2" -dependencies = [ - "bytes", - "futures-core", - "futures-io", - "futures-sink", - "futures-util", - "hashbrown", - "pin-project-lite", - "slab", - "tokio", - "tracing", -] - -[[package]] -name = "toml" -version = "0.5.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" -dependencies = [ - "serde", -] - -[[package]] -name = "toml_datetime" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4553f467ac8e3d374bc9a177a26801e5d0f9b211aa1673fb137a403afd1c9cf5" - -[[package]] -name = "toml_edit" -version = "0.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56c59d8dd7d0dcbc6428bf7aa2f0e823e26e43b3c9aca15bbc9475d23e5fa12b" -dependencies = [ - "indexmap", - "nom8", - "toml_datetime", -] - -[[package]] -name = "tracing" -version = "0.1.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" -dependencies = [ - "cfg-if", - "pin-project-lite", - "tracing-attributes", - "tracing-core", -] - -[[package]] -name = "tracing-attributes" -version = "0.1.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4017f8f45139870ca7e672686113917c71c7a6e02d4924eda67186083c03081a" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "tracing-core" -version = "0.1.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a" -dependencies = [ - "once_cell", -] - -[[package]] -name = "uds_windows" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce65604324d3cce9b966701489fbd0cf318cb1f7bd9dd07ac9a4ee6fb791930d" -dependencies = [ - "tempfile", - "winapi", -] - -[[package]] -name = "unicode-ident" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" - -[[package]] -name = "unicode-width" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" - -[[package]] -name = "usbredirhost" -version = "0.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87485e4dfeb0176203afd1086f11ed2ead837053143b12b6eed55c598e9393d5" -dependencies = [ - "libc", - "rusb", - "usbredirhost-sys", - "usbredirparser", -] - -[[package]] -name = "usbredirhost-sys" -version = "0.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b27c305da1f7601b665d68948bcfaf9909d443bec94510ab776118ab8afc2c7d" -dependencies = [ - "libusb1-sys", - "pkg-config", - "usbredirparser-sys", -] - -[[package]] -name = "usbredirparser" -version = "0.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0f8b5241d7cbb3e08b4677212a9ac001f116f50731c2737d16129a84ecf6a56" -dependencies = [ - "libc", - "usbredirparser-sys", -] - -[[package]] -name = "usbredirparser-sys" -version = "0.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8b0e834e187916fc762bccdc9d64e454a0ee58b134f8f7adab321141e8e0d91" -dependencies = [ - "pkg-config", -] - -[[package]] -name = "vcpkg" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" - -[[package]] -name = "version_check" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" - -[[package]] -name = "waker-fn" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca" - -[[package]] -name = "walkdir" -version = "2.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" -dependencies = [ - "same-file", - "winapi", - "winapi-util", -] - -[[package]] -name = "wasi" -version = "0.10.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" - -[[package]] -name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" - -[[package]] -name = "wasm-bindgen" -version = "0.2.84" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31f8dcbc21f30d9b8f2ea926ecb58f6b91192c17e9d33594b3df58b2007ca53b" -dependencies = [ - "cfg-if", - "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.84" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95ce90fd5bcc06af55a641a86428ee4229e44e07033963a2290a8e241607ccb9" -dependencies = [ - "bumpalo", - "log", - "once_cell", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.84" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c21f77c0bedc37fd5dc21f897894a5ca01e7bb159884559461862ae90c0b4c5" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.84" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-backend", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.84" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d" - -[[package]] -name = "weezl" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9193164d4de03a926d909d3bc7c30543cecb35400c02114792c2cae20d5e2dbb" - -[[package]] -name = "wepoll-ffi" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d743fdedc5c64377b5fc2bc036b01c7fd642205a0d96356034ae3404d49eb7fb" -dependencies = [ - "cc", -] - -[[package]] -name = "which" -version = "4.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2441c784c52b289a054b7201fc93253e288f094e2f4be9058343127c4226a269" -dependencies = [ - "either", - "libc", - "once_cell", -] - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-util" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" -dependencies = [ - "winapi", -] - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "windows" -version = "0.43.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04662ed0e3e5630dfa9b26e4cb823b817f1a9addda855d973a9458c236556244" -dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", -] - -[[package]] -name = "windows-sys" -version = "0.42.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" -dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", -] - -[[package]] -name = "windows-sys" -version = "0.45.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" -dependencies = [ - "windows-targets", -] - -[[package]] -name = "windows-targets" -version = "0.42.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e2522491fbfcd58cc84d47aeb2958948c4b8982e9a2d8a2a35bbaed431390e7" -dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.42.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.42.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7" - -[[package]] -name = "windows_i686_gnu" -version = "0.42.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640" - -[[package]] -name = "windows_i686_msvc" -version = "0.42.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.42.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.42.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.42.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd" - -[[package]] -name = "winreg" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2986deb581c4fe11b621998a5e53361efe6b48a151178d0cd9eeffa4dc6acc9" -dependencies = [ - "winapi", -] - -[[package]] -name = "xml-rs" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3" - -[[package]] -name = "zbus" -version = "3.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41ce2de393c874ba871292e881bf3c13a0d5eb38170ebab2e50b4c410eaa222b" -dependencies = [ - "async-broadcast 0.4.1", - "async-channel", - "async-executor", - "async-io", - "async-lock", - "async-recursion", - "async-task", - "async-trait", - "byteorder", - "derivative", - "dirs", - "enumflags2", - "event-listener", - "futures-core", - "futures-sink", - "futures-util", - "hex", - "nix 0.24.3", - "once_cell", - "ordered-stream", - "rand", - "serde", - "serde-xml-rs", - "serde_repr", - "sha1", - "static_assertions", - "tracing", - "uds_windows", - "winapi", - "zbus_macros", - "zbus_names", - "zvariant", -] - -[[package]] -name = "zbus_macros" -version = "3.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a13d08f5dc6cf725b693cb6ceacd43cd430ec0664a879188f29e7d7dcd98f96d" -dependencies = [ - "proc-macro-crate", - "proc-macro2", - "quote", - "regex", - "syn", -] - -[[package]] -name = "zbus_names" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f34f314916bd89bdb9934154627fab152f4f28acdda03e7c4c68181b214fe7e3" -dependencies = [ - "serde", - "static_assertions", - "zvariant", -] - -[[package]] -name = "zstd" -version = "0.9.2+zstd.1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2390ea1bf6c038c39674f22d95f0564725fc06034a47129179810b2fc58caa54" -dependencies = [ - "zstd-safe", -] - -[[package]] -name = "zstd-safe" -version = "4.1.3+zstd.1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e99d81b99fb3c2c2c794e3fe56c305c63d5173a16a46b5850b07c935ffc7db79" -dependencies = [ - "libc", - "zstd-sys", -] - -[[package]] -name = "zstd-sys" -version = "1.6.2+zstd.1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2daf2f248d9ea44454bfcb2516534e8b8ad2fc91bf818a1885495fc42bc8ac9f" -dependencies = [ - "cc", - "libc", -] - -[[package]] -name = "zune-inflate" -version = "0.2.51" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a01728b79fb9b7e28a8c11f715e1cd8dc2cda7416a007d66cac55cebb3a8ac6b" -dependencies = [ - "simd-adler32", -] - -[[package]] -name = "zvariant" -version = "3.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "903169c05b9ab948ee93fefc9127d08930df4ce031d46c980784274439803e51" -dependencies = [ - "byteorder", - "enumflags2", - "libc", - "serde", - "serde_bytes", - "static_assertions", - "zvariant_derive", -] - -[[package]] -name = "zvariant_derive" -version = "3.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cce76636e8fab7911be67211cf378c252b115ee7f2bae14b18b84821b39260b5" -dependencies = [ - "proc-macro-crate", - "proc-macro2", - "quote", - "syn", -] diff --git a/vdi/host/Cargo.toml b/vdi/host/Cargo.toml deleted file mode 100644 index 0584b469018d..000000000000 --- a/vdi/host/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "qemu-rustdesk" -version = "0.1.0" -authors = ["rustdesk "] -edition = "2021" - -[dependencies] -qemu-display = { git = "https://github.com/rustdesk/qemu-display" } -hbb_common = { path = "../../libs/hbb_common" } -clap = { version = "4.1", features = ["derive"] } -zbus = { version = "3.14.1" } -image = "0.24" -async-trait = "0.1" diff --git a/vdi/host/README.md b/vdi/host/README.md deleted file mode 100644 index 0283266bf7dd..000000000000 --- a/vdi/host/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# RustDesk protocol on QEMU D-Bus display - -``` -sudo apt install libusbredirparser-dev libusbredirhost-dev libusb-1.0-0-dev -``` diff --git a/vdi/host/src/connection.rs b/vdi/host/src/connection.rs deleted file mode 100644 index 9f856fa2e5ec..000000000000 --- a/vdi/host/src/connection.rs +++ /dev/null @@ -1,11 +0,0 @@ -use hbb_common::{message_proto::*, tokio, ResultType}; -pub use tokio::sync::{mpsc, Mutex}; -pub struct Connection { - pub tx: mpsc::UnboundedSender, -} - -impl Connection { - pub async fn on_message(&mut self, message: Message) -> ResultType { - Ok(true) - } -} diff --git a/vdi/host/src/console.rs b/vdi/host/src/console.rs deleted file mode 100644 index a342f1a9afc0..000000000000 --- a/vdi/host/src/console.rs +++ /dev/null @@ -1,119 +0,0 @@ -use hbb_common::{tokio, ResultType}; -use image::GenericImage; -use qemu_display::{Console, ConsoleListenerHandler, MouseButton}; -use std::{collections::HashSet, sync::Arc}; -pub use tokio::sync::{mpsc, Mutex}; - -#[derive(Debug)] -pub enum Event { - ConsoleUpdate((i32, i32, i32, i32)), - Disconnected, -} - -const PIXMAN_X8R8G8B8: u32 = 0x20020888; -pub type BgraImage = image::ImageBuffer, Vec>; -#[derive(Debug)] -pub struct ConsoleListener { - pub image: Arc>, - pub tx: mpsc::UnboundedSender, -} - -#[async_trait::async_trait] -impl ConsoleListenerHandler for ConsoleListener { - async fn scanout(&mut self, s: qemu_display::Scanout) { - *self.image.lock().await = image_from_vec(s.format, s.width, s.height, s.stride, s.data); - } - - async fn update(&mut self, u: qemu_display::Update) { - let update = image_from_vec(u.format, u.w as _, u.h as _, u.stride, u.data); - let mut image = self.image.lock().await; - if (u.x, u.y) == (0, 0) && update.dimensions() == image.dimensions() { - *image = update; - } else { - image.copy_from(&update, u.x as _, u.y as _).unwrap(); - } - self.tx - .send(Event::ConsoleUpdate((u.x, u.y, u.w, u.h))) - .ok(); - } - - async fn scanout_dmabuf(&mut self, _scanout: qemu_display::ScanoutDMABUF) { - unimplemented!() - } - - async fn update_dmabuf(&mut self, _update: qemu_display::UpdateDMABUF) { - unimplemented!() - } - - async fn mouse_set(&mut self, set: qemu_display::MouseSet) { - dbg!(set); - } - - async fn cursor_define(&mut self, cursor: qemu_display::Cursor) { - dbg!(cursor); - } - - fn disconnected(&mut self) { - self.tx.send(Event::Disconnected).ok(); - } -} - -pub async fn key_event(console: &mut Console, qnum: u32, down: bool) -> ResultType<()> { - if down { - console.keyboard.press(qnum).await?; - } else { - console.keyboard.release(qnum).await?; - } - Ok(()) -} - -fn image_from_vec(format: u32, width: u32, height: u32, stride: u32, data: Vec) -> BgraImage { - if format != PIXMAN_X8R8G8B8 { - todo!("unhandled pixman format: {}", format) - } - if cfg!(target_endian = "big") { - todo!("pixman/image in big endian") - } - let layout = image::flat::SampleLayout { - channels: 4, - channel_stride: 1, - width, - width_stride: 4, - height, - height_stride: stride as _, - }; - let samples = image::flat::FlatSamples { - samples: data, - layout, - color_hint: None, - }; - samples - .try_into_buffer::>() - .or_else::<&str, _>(|(_err, samples)| { - let view = samples.as_view::>().unwrap(); - let mut img = BgraImage::new(width, height); - img.copy_from(&view, 0, 0).unwrap(); - Ok(img) - }) - .unwrap() -} - -fn button_mask_to_set(mask: u8) -> HashSet { - let mut set = HashSet::new(); - if mask & 0b0000_0001 != 0 { - set.insert(MouseButton::Left); - } - if mask & 0b0000_0010 != 0 { - set.insert(MouseButton::Middle); - } - if mask & 0b0000_0100 != 0 { - set.insert(MouseButton::Right); - } - if mask & 0b0000_1000 != 0 { - set.insert(MouseButton::WheelUp); - } - if mask & 0b0001_0000 != 0 { - set.insert(MouseButton::WheelDown); - } - set -} diff --git a/vdi/host/src/lib.rs b/vdi/host/src/lib.rs deleted file mode 100644 index e9f8d7ed3cfa..000000000000 --- a/vdi/host/src/lib.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod server; -mod console; -mod connection; diff --git a/vdi/host/src/main.rs b/vdi/host/src/main.rs deleted file mode 100644 index ea32a028a3c5..000000000000 --- a/vdi/host/src/main.rs +++ /dev/null @@ -1,6 +0,0 @@ -fn main() { - hbb_common::init_log(false, ""); - if let Err(err) = qemu_rustdesk::server::run() { - hbb_common::log::error!("{err}"); - } -} diff --git a/vdi/host/src/server.rs b/vdi/host/src/server.rs deleted file mode 100644 index b43bd364f46b..000000000000 --- a/vdi/host/src/server.rs +++ /dev/null @@ -1,172 +0,0 @@ -use clap::Parser; -use hbb_common::{ - allow_err, - anyhow::{bail, Context}, - log, - message_proto::*, - protobuf::Message as _, - tokio, - tokio::net::TcpListener, - ResultType, Stream, -}; -use qemu_display::{Console, VMProxy}; -use std::{borrow::Borrow, sync::Arc}; - -use crate::connection::*; -use crate::console::*; - -#[derive(Parser, Debug)] -pub struct SocketAddrArgs { - /// IP address - #[clap(short, long, default_value = "0.0.0.0")] - address: std::net::IpAddr, - /// IP port number - #[clap(short, long, default_value = "21116")] - port: u16, -} - -impl From for std::net::SocketAddr { - fn from(args: SocketAddrArgs) -> Self { - (args.address, args.port).into() - } -} - -#[derive(Parser, Debug)] -struct Cli { - #[clap(flatten)] - address: SocketAddrArgs, - #[clap(short, long)] - dbus_address: Option, -} - -#[derive(Debug)] -struct Server { - vm_name: String, - rx_console: mpsc::UnboundedReceiver, - tx_console: mpsc::UnboundedSender, - rx_conn: mpsc::UnboundedReceiver, - tx_conn: mpsc::UnboundedSender, - image: Arc>, - console: Arc>, -} - -impl Server { - async fn new(vm_name: String, console: Console) -> ResultType { - let width = console.width().await?; - let height = console.height().await?; - let image = BgraImage::new(width as _, height as _); - let (tx_console, rx_console) = mpsc::unbounded_channel(); - let (tx_conn, rx_conn) = mpsc::unbounded_channel(); - Ok(Self { - vm_name, - rx_console, - tx_console, - rx_conn, - tx_conn, - image: Arc::new(Mutex::new(image)), - console: Arc::new(Mutex::new(console)), - }) - } - - async fn stop_console(&self) -> ResultType<()> { - self.console.lock().await.unregister_listener(); - Ok(()) - } - - async fn run_console(&self) -> ResultType<()> { - self.console - .lock() - .await - .register_listener(ConsoleListener { - image: self.image.clone(), - tx: self.tx_console.clone(), - }) - .await?; - Ok(()) - } - - async fn dimensions(&self) -> (u16, u16) { - let image = self.image.lock().await; - (image.width() as u16, image.height() as u16) - } - - async fn handle_connection(&mut self, stream: Stream) -> ResultType<()> { - let mut stream = stream; - self.run_console().await?; - let mut conn = Connection { - tx: self.tx_conn.clone(), - }; - - loop { - tokio::select! { - Some(evt) = self.rx_console.recv() => { - match evt { - _ => {} - } - } - Some(msg) = self.rx_conn.recv() => { - allow_err!(stream.send(&msg).await); - } - res = stream.next() => { - if let Some(res) = res { - match res { - Err(err) => { - bail!(err); - } - Ok(bytes) => { - if let Ok(msg_in) = Message::parse_from_bytes(&bytes) { - match conn.on_message(msg_in).await { - Ok(false) => { - break; - } - Err(err) => { - log::error!("{err}"); - } - _ => {} - } - } - } - } - } else { - bail!("Reset by the peer"); - } - } - } - } - - self.stop_console().await?; - Ok(()) - } -} - -#[tokio::main] -pub async fn run() -> ResultType<()> { - let args = Cli::parse(); - - let listener = TcpListener::bind::(args.address.into()) - .await - .unwrap(); - let dbus = if let Some(addr) = args.dbus_address { - zbus::ConnectionBuilder::address(addr.borrow())? - .build() - .await - } else { - zbus::Connection::session().await - } - .context("Failed to connect to DBus")?; - - let vm_name = VMProxy::new(&dbus).await?.name().await?; - let console = Console::new(&dbus.into(), 0) - .await - .context("Failed to get the console")?; - let mut server = Server::new(format!("qemu-rustdesk ({})", vm_name), console).await?; - loop { - let (stream, addr) = listener.accept().await?; - stream.set_nodelay(true).ok(); - let laddr = stream.local_addr()?; - let stream = Stream::from(stream, laddr); - if let Err(err) = server.handle_connection(stream).await { - log::error!("Connection from {addr} closed: {err}"); - } - } -} From 05b0f95b793878d0fd728feb3153e508f2371b94 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Thu, 30 Jan 2025 13:53:02 +0800 Subject: [PATCH 541/541] restore entrypoint.sh --- entrypoint.sh | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100755 entrypoint.sh diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100755 index 000000000000..8c7be0786ed5 --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,36 @@ +#!/bin/sh + +cd "$HOME"/rustdesk || exit 1 +# shellcheck source=/dev/null +. "$HOME"/.cargo/env + +argv=$* + +while test $# -gt 0; do + case "$1" in + --release) + mkdir -p target/release + test -f target/release/libsciter-gtk.so || cp "$HOME"/libsciter-gtk.so target/release/ + release=1 + shift + ;; + --target) + shift + if test $# -gt 0; then + rustup target add "$1" + shift + fi + ;; + *) + shift + ;; + esac +done + +if [ -z $release ]; then + mkdir -p target/debug + test -f target/debug/libsciter-gtk.so || cp "$HOME"/libsciter-gtk.so target/debug/ +fi +set -f +#shellcheck disable=2086 +VCPKG_ROOT=/vcpkg cargo build $argv

    1g!6X=?)t>ew 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%0AK%_v0^{jiP4q03qwph?;u=)*a|I{)#i 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